From 4ec9b55a793c93fe2868326a29fc8607711118f4 Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Thu, 13 Sep 2018 11:53:17 +0200 Subject: [PATCH 01/31] [FIX] web: background layout landscape no +1 page The report layout "background" has an image background from bottom to top. When we transform the report in PDF, we have no idea in the document of how many page, which orientation, width or height it will take. Thus to be able to have the background at the bottom of a page when there is not enough content, we set a min-height for the body content. This height was 1000px originally and has been decreased to 900px in 41c75ad0c3 because an empty page was possibly added (since the height of body + header + footer was bigger than page size). This issue is about getting a blank page when pdf is in landscape. For example in A4 with 90ppi has a 1052px height in portrait, but 744px height in landscape so we always get at least two pages with possibly a blank one. With this commit we lower it to 620px allowing 124px headers/footers. opw-1881059 closes #26962 --- addons/web/static/src/less/layout_background.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web/static/src/less/layout_background.less b/addons/web/static/src/less/layout_background.less index 6750705c35b..d5a26faa843 100644 --- a/addons/web/static/src/less/layout_background.less +++ b/addons/web/static/src/less/layout_background.less @@ -34,7 +34,7 @@ background-size: cover; background-position: bottom center; background-repeat: no-repeat; - min-height: 900px; + min-height: 620px; thead tr th, h2 { color: @odoo-brand-primary; } From 0c8a244e68b14f71b348886047b487b67fed454d Mon Sep 17 00:00:00 2001 From: Toufik Benjaa Date: Thu, 13 Sep 2018 15:48:09 +0200 Subject: [PATCH 02/31] [FIX] mass_mailing: check duplicate recipients on mass mailing - When checking if a recipient has already received a mass mailing, the record model for the recipient was not checked. Which leads to false positives. --- addons/mass_mailing/models/mass_mailing.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index 83a4cb2278a..00dfa06838b 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -610,10 +610,11 @@ class MassMailing(models.Model): """ else: query +=""" - AND s.mass_mailing_id = %%(mailing_id)s; + AND s.mass_mailing_id = %%(mailing_id)s + AND s.model = %%(target_model)s; """ query = query % {'target': target._table, 'mail_field': mail_field} - params = {'mailing_id': self.id, 'mailing_campaign_id': self.mass_mailing_campaign_id.id} + params = {'mailing_id': self.id, 'mailing_campaign_id': self.mass_mailing_campaign_id.id, 'target_model': self.mailing_model_real} self._cr.execute(query, params) seen_list = set(m[0] for m in self._cr.fetchall()) _logger.info( From 7dc13e237cf7860fa7490920322c9416456f1b93 Mon Sep 17 00:00:00 2001 From: Olivier Colson Date: Thu, 13 Sep 2018 15:29:41 +0200 Subject: [PATCH 03/31] [FIX] l10n_es: apply correct type to each account. --- addons/l10n_es/account_account_common.xml | 162 +++++++++++----------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/addons/l10n_es/account_account_common.xml b/addons/l10n_es/account_account_common.xml index e1ac2a4866f..4bc27cf90f8 100644 --- a/addons/l10n_es/account_account_common.xml +++ b/addons/l10n_es/account_account_common.xml @@ -16,413 +16,413 @@ 101 Fondo social - + 102 Capital - + 108 Acciones o participaciones propias en situaciones especiales - + 109 Acciones o participaciones propias para reducción de capital - + 110 Prima de emisión o asunción - + 112 Reserva legal - + 113 Reservas voluntarias - + 1140 Reservas para acciones o participaciones de la sociedad dominante - + 1141 Reservas estatutarias - + 1142 Reserva por capital amortizado - + 1144 Reservas por acciones propias aceptadas en garantía - + 118 Aportaciones de socios o propietarios - + 119 Diferencias por ajuste del capital a euros - + 131 Donaciones y legados de capital - + 1370 Ingresos fiscales por diferencias permanentes a distribuir en varios ejercicios - + 1371 Ingresos fiscales por deducciones y bonificaciones a distribuir en varios ejercicios - + 141 Provisión para impuestos - + 142 Provisión para otras responsabilidades - + 143 Provisión por desmantelamiento, retiro o rehabilitación del inmovilizado - + 145 Provisión para actuaciones medioambientales - + 150 Acciones o participaciones consideradas como pasivos financieros - + 1533 Desembolsos no exigidos, empresas del grupo - + 1534 Desembolsos no exigidos, empresas asociadas - + 1535 Desembolsos no exigidos, otras partes vinculadas - + 1536 Otros desembolsos no exigidos - + 1543 Aportaciones no dinerarias pendientes, empresas del grupo - + 1544 Aportaciones no dinerarias pendientes, empresas asociadas - + 1545 Aportaciones no dinerarias pendientes, otras partes vinculadas - + 1546 Otras aportaciones no dinerarias pendientes - + 1603 Deudas con entidades de crédito vinculadas, empresas del grupo - + 1604 Deudas con entidades de crédito vinculadas, empresas asociadas - + 1605 Deudas con otras entidades de crédito vinculadas - + 1613 Proveedores de inmovilizado, empresas del grupo - + 1614 Proveedores de inmovilizado, empresas asociadas - + 1615 Proveedores de inmovilizado, otras partes vinculadas - + 1623 Acreedores por arrendamiento financiero, empresas de grupo - + 1624 Acreedores por arrendamiento financiero, empresas asociadas - + 1625 Acreedores por arrendamiento financiero, otras partes vinculadas - + 1633 Otras deudas, empresas del grupo - + 1634 Otras deudas, empresas asociadas - + 1635 Otras deudas, con otras partes vinculadas - + 170 Deudas con entidades de crédito - + 171 Deudas - + 172 Deudas transformables en subvenciones, donaciones y legados - + 173 Proveedores de inmovilizado - + 174 Acreedores por arrendamiento financiero - + 175 Efectos a pagar - + 176 Pasivos por derivados financieros - + 177 Obligaciones y bonos - + 179 Deudas representadas en otros valores negociables - + 180 Fianzas recibidas - + 181 Anticipos recibidos por ventas o prestaciones de servicios - + 185 Depósitos recibidos - + 190 Acciones o participaciones emitidas - + 192 Suscriptores de acciones - + 194 Capital emitido pendiente de inscripción - + 195 Acciones o participaciones emitidas consideradas como pasivos financieros - + 197 Suscriptores de acciones consideradas como pasivos financieros - + 199 Acciones o participaciones emitidas consideradas como pasivos financieros pendientes de inscripción - + @@ -1246,9 +1246,9 @@ 401 - + Proveedores, efectos comerciales a pagar - + @@ -1288,30 +1288,30 @@ 404 - + Proveedores, empresas asociadas - + 405 - + Proveedores, otras partes vinculadas - + 406 - + Envases y embalajes a devolver a proveedores - + 407 - + Anticipos a proveedores - + @@ -1400,9 +1400,9 @@ 432 - + Clientes, operaciones de factoring - + @@ -1456,37 +1456,37 @@ 434 - + Clientes, empresas asociadas - + 435 - + Clientes, otras partes vinculadas - + 436 - + Clientes de dudoso cobro - + 437 - + Envases y embalajes a devolver por clientes - + 438 - + Anticipos de clientes - + From c568af83a2273e82df5d1f5b9e8d2c1e399a30d8 Mon Sep 17 00:00:00 2001 From: Olivier Colson Date: Thu, 13 Sep 2018 16:28:01 +0200 Subject: [PATCH 04/31] [FIX] l10n_es: apply correct type to each account. --- .../account_account_template_assoc_data.xml | 22 +-- .../account_account_template_common_data.xml | 162 +++++++++--------- .../account_account_template_full_data.xml | 50 +++--- .../account_account_template_pymes_data.xml | 18 +- 4 files changed, 126 insertions(+), 126 deletions(-) diff --git a/addons/l10n_es/data/account_account_template_assoc_data.xml b/addons/l10n_es/data/account_account_template_assoc_data.xml index 1226d986fed..6efb136cf5d 100644 --- a/addons/l10n_es/data/account_account_template_assoc_data.xml +++ b/addons/l10n_es/data/account_account_template_assoc_data.xml @@ -6,7 +6,7 @@ 100 Dotación fundacional - + @@ -14,7 +14,7 @@ 1030 Fundadores, parte no desembolsada en fundaciones - + @@ -22,7 +22,7 @@ 1034 Asociados, parte no desembolsada en asociaciones - + @@ -30,7 +30,7 @@ 1040 Fundadores, por aportaciones no dinerarias pendientes, en fundaciones - + @@ -38,7 +38,7 @@ 1044 Asociados, por aportaciones no dinerarias pendientes, en asociaciones - + @@ -46,7 +46,7 @@ 120 Remanente - + @@ -54,7 +54,7 @@ 121 Resultados negativos de ejercicios anteriores - + @@ -70,7 +70,7 @@ 1300 Subvenciones del Estado - + @@ -78,7 +78,7 @@ 1301 Subvenciones de otras Administraciones Públicas - + @@ -86,7 +86,7 @@ 1320 Otras subvenciones - + @@ -94,7 +94,7 @@ 1321 Otras donaciones y legados - + diff --git a/addons/l10n_es/data/account_account_template_common_data.xml b/addons/l10n_es/data/account_account_template_common_data.xml index 1a489efb07d..d7e538bc8b6 100644 --- a/addons/l10n_es/data/account_account_template_common_data.xml +++ b/addons/l10n_es/data/account_account_template_common_data.xml @@ -5,7 +5,7 @@ 101 Fondo social - + @@ -13,7 +13,7 @@ 102 Capital - + @@ -21,7 +21,7 @@ 108 Acciones o participaciones propias en situaciones especiales - + @@ -29,7 +29,7 @@ 109 Acciones o participaciones propias para reducción de capital - + @@ -37,7 +37,7 @@ 110 Prima de emisión o asunción - + @@ -45,7 +45,7 @@ 112 Reserva legal - + @@ -53,7 +53,7 @@ 113 Reservas voluntarias - + @@ -61,7 +61,7 @@ 1140 Reservas para acciones o participaciones de la sociedad dominante - + @@ -69,7 +69,7 @@ 1141 Reservas estatutarias - + @@ -77,7 +77,7 @@ 1142 Reserva por capital amortizado - + @@ -85,7 +85,7 @@ 1144 Reservas por acciones propias aceptadas en garantía - + @@ -93,7 +93,7 @@ 118 Aportaciones de socios o propietarios - + @@ -101,7 +101,7 @@ 119 Diferencias por ajuste del capital a euros - + @@ -109,7 +109,7 @@ 131 Donaciones y legados de capital - + @@ -117,7 +117,7 @@ 1370 Ingresos fiscales por diferencias permanentes a distribuir en varios ejercicios - + @@ -125,7 +125,7 @@ 1371 Ingresos fiscales por deducciones y bonificaciones a distribuir en varios ejercicios - + @@ -133,7 +133,7 @@ 141 Provisión para impuestos - + @@ -141,7 +141,7 @@ 142 Provisión para otras responsabilidades - + @@ -149,7 +149,7 @@ 143 Provisión por desmantelamiento, retiro o rehabilitación del inmovilizado - + @@ -157,7 +157,7 @@ 145 Provisión para actuaciones medioambientales - + @@ -165,7 +165,7 @@ 150 Acciones o participaciones consideradas como pasivos financieros - + @@ -173,7 +173,7 @@ 1533 Desembolsos no exigidos, empresas del grupo - + @@ -181,7 +181,7 @@ 1534 Desembolsos no exigidos, empresas asociadas - + @@ -189,7 +189,7 @@ 1535 Desembolsos no exigidos, otras partes vinculadas - + @@ -197,7 +197,7 @@ 1536 Otros desembolsos no exigidos - + @@ -205,7 +205,7 @@ 1543 Aportaciones no dinerarias pendientes, empresas del grupo - + @@ -213,7 +213,7 @@ 1544 Aportaciones no dinerarias pendientes, empresas asociadas - + @@ -221,7 +221,7 @@ 1545 Aportaciones no dinerarias pendientes, otras partes vinculadas - + @@ -229,7 +229,7 @@ 1546 Otras aportaciones no dinerarias pendientes - + @@ -237,7 +237,7 @@ 1603 Deudas con entidades de crédito vinculadas, empresas del grupo - + @@ -245,7 +245,7 @@ 1604 Deudas con entidades de crédito vinculadas, empresas asociadas - + @@ -253,7 +253,7 @@ 1605 Deudas con otras entidades de crédito vinculadas - + @@ -261,7 +261,7 @@ 1613 Proveedores de inmovilizado, empresas del grupo - + @@ -269,7 +269,7 @@ 1614 Proveedores de inmovilizado, empresas asociadas - + @@ -277,7 +277,7 @@ 1615 Proveedores de inmovilizado, otras partes vinculadas - + @@ -285,7 +285,7 @@ 1623 Acreedores por arrendamiento financiero, empresas de grupo - + @@ -293,7 +293,7 @@ 1624 Acreedores por arrendamiento financiero, empresas asociadas - + @@ -301,7 +301,7 @@ 1625 Acreedores por arrendamiento financiero, otras partes vinculadas - + @@ -309,7 +309,7 @@ 1633 Otras deudas, empresas del grupo - + @@ -317,7 +317,7 @@ 1634 Otras deudas, empresas asociadas - + @@ -325,7 +325,7 @@ 1635 Otras deudas, con otras partes vinculadas - + @@ -333,7 +333,7 @@ 170 Deudas con entidades de crédito - + @@ -341,7 +341,7 @@ 171 Deudas - + @@ -349,7 +349,7 @@ 172 Deudas transformables en subvenciones, donaciones y legados - + @@ -357,7 +357,7 @@ 173 Proveedores de inmovilizado - + @@ -365,7 +365,7 @@ 174 Acreedores por arrendamiento financiero - + @@ -373,7 +373,7 @@ 175 Efectos a pagar - + @@ -381,7 +381,7 @@ 176 Pasivos por derivados financieros - + @@ -389,7 +389,7 @@ 177 Obligaciones y bonos - + @@ -397,7 +397,7 @@ 179 Deudas representadas en otros valores negociables - + @@ -405,7 +405,7 @@ 180 Fianzas recibidas - + @@ -413,7 +413,7 @@ 181 Anticipos recibidos por ventas o prestaciones de servicios - + @@ -421,7 +421,7 @@ 185 Depósitos recibidos - + @@ -429,7 +429,7 @@ 190 Acciones o participaciones emitidas - + @@ -437,7 +437,7 @@ 192 Suscriptores de acciones - + @@ -445,7 +445,7 @@ 194 Capital emitido pendiente de inscripción - + @@ -453,7 +453,7 @@ 195 Acciones o participaciones emitidas consideradas como pasivos financieros - + @@ -461,7 +461,7 @@ 197 Suscriptores de acciones consideradas como pasivos financieros - + @@ -469,7 +469,7 @@ 199 Acciones o participaciones emitidas consideradas como pasivos financieros pendientes de inscripción - + @@ -1411,9 +1411,9 @@ 401 - + Proveedores, efectos comerciales a pagar - + @@ -1459,33 +1459,33 @@ 404 - + Proveedores, empresas asociadas - + 405 - + Proveedores, otras partes vinculadas - + 406 - + Envases y embalajes a devolver a proveedores - + 407 - + Anticipos a proveedores - + @@ -1587,9 +1587,9 @@ 432 - + Clientes, operaciones de factoring - + @@ -1651,41 +1651,41 @@ 434 - + Clientes, empresas asociadas - + 435 - + Clientes, otras partes vinculadas - + 436 - + Clientes de dudoso cobro - + 437 - + Envases y embalajes a devolver por clientes - + 438 - + Anticipos de clientes - + diff --git a/addons/l10n_es/data/account_account_template_full_data.xml b/addons/l10n_es/data/account_account_template_full_data.xml index 61d5bc4b0c8..ae643434a38 100644 --- a/addons/l10n_es/data/account_account_template_full_data.xml +++ b/addons/l10n_es/data/account_account_template_full_data.xml @@ -6,7 +6,7 @@ 100 Capital social - + @@ -14,7 +14,7 @@ 1030 Socios por desembolsos no exigidos, capital social - + @@ -22,7 +22,7 @@ 1034 Socios por desembolsos no exigidos, capital pendiente de inscripción - + @@ -30,7 +30,7 @@ 1040 Socios por aportaciones no dinerarias pendientes, capital social - + @@ -38,7 +38,7 @@ 1044 Socios por aportaciones no dinerarias pendientes, capital pendiente de inscripción - + @@ -46,7 +46,7 @@ 1110 Patrimonio neto por emision de instrumentos financieros compuestos - + @@ -54,7 +54,7 @@ 111100 Resto de instrumentos de patrimonio neto - + @@ -62,7 +62,7 @@ 1143 Reserva por fondo de comercio - + @@ -70,7 +70,7 @@ 115 Reservas por pérdidas y ganancias actuariales y otros ajustes - + @@ -78,7 +78,7 @@ 120 Remanente - + @@ -86,7 +86,7 @@ 121 Resultados negativos de ejercicios anteriores - + @@ -102,7 +102,7 @@ 130 Subvenciones oficiales de capital - + @@ -110,7 +110,7 @@ 132 Otras subvenciones, donaciones y legados - + @@ -118,7 +118,7 @@ 133 Ajustes por valoración en activos financieros disponibles para la venta - + @@ -126,7 +126,7 @@ 1340 Cobertura de flujos de efectivo - + @@ -134,7 +134,7 @@ 1341 Cobertura de una inversión neta en un negocio en el extranjero - + @@ -142,7 +142,7 @@ 135 Diferencias de conversión - + @@ -150,7 +150,7 @@ 136 Ajustes por valoración de activos no corrientes y grupos enajenables de elementos, mantenidos para la venta - + @@ -158,7 +158,7 @@ 140 Provisión por retribuciones del personal - + @@ -166,7 +166,7 @@ 146 Provisión para reestructuraciones - + @@ -174,7 +174,7 @@ 147 Provisión por transacciones con pagos basados en instrumentos de patrimonio - + @@ -182,7 +182,7 @@ 1765 Pasivos por derivados financieros, carter de negociación - + @@ -190,7 +190,7 @@ 1768 Pasivos por derivados financieros, instrumentos de cobertura - + @@ -198,7 +198,7 @@ 178 Obligaciones y bonos convertibles - + @@ -206,7 +206,7 @@ 189 Garantías financieras - + diff --git a/addons/l10n_es/data/account_account_template_pymes_data.xml b/addons/l10n_es/data/account_account_template_pymes_data.xml index 716b61a6752..87fa44cca5c 100644 --- a/addons/l10n_es/data/account_account_template_pymes_data.xml +++ b/addons/l10n_es/data/account_account_template_pymes_data.xml @@ -6,7 +6,7 @@ 100 Capital social - + @@ -14,7 +14,7 @@ 1030 Socios por desembolsos no exigidos, capital social - + @@ -22,7 +22,7 @@ 1034 Socios por desembolsos no exigidos, capital pendiente de inscripción - + @@ -30,7 +30,7 @@ 1040 Socios por aportaciones no dinerarias pendientes, capital social - + @@ -38,7 +38,7 @@ 1044 Socios por aportaciones no dinerarias pendientes, capital pendiente de inscripción - + @@ -46,7 +46,7 @@ 120 Remanente - + @@ -54,7 +54,7 @@ 121 Resultados negativos de ejercicios anteriores - + @@ -70,7 +70,7 @@ 130 Subvenciones oficiales de capital - + @@ -78,7 +78,7 @@ 132 Otras subvenciones, donaciones y legados - + From 148182bf912c10faa5a562378fa3c25765f3e85a Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Tue, 4 Sep 2018 17:08:03 +0200 Subject: [PATCH 05/31] [FIX] website: seo add words no freeze On a website page, click on promote and add the keyword "DS1517" before this commit, the javascript froze after this commit, it doesn't freeze OPW 1880341 closes #26771 --- addons/website/static/src/js/menu/seo.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/website/static/src/js/menu/seo.js b/addons/website/static/src/js/menu/seo.js index 0909b6c5ad5..696a7d76f78 100644 --- a/addons/website/static/src/js/menu/seo.js +++ b/addons/website/static/src/js/menu/seo.js @@ -95,7 +95,8 @@ var SuggestionList = Widget.extend({ var self = this; self.$el.empty(); // TODO Improve algorithm + Ajust based on custom user keywords - var regex = new RegExp(self.root, 'gi'); + var regex_string = this.root + '\\+?'; + var regex = new RegExp(regex_string, 'gi'); keywords = _.map(_.uniq(keywords), function (word) { return word.replace(regex, '').trim(); }); From 9511cb2bb0fe41d2759dc504c7f5cae7f6ed0c99 Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Thu, 13 Sep 2018 15:28:46 +0200 Subject: [PATCH 06/31] [FIX] point_of_sale: param for reconcile orders and payment It has been argued that a regression took place because of e2efec8e544917e8524f75739eaf7cfabc1e23ae Before that commit, everything was reconciled together After, only payments and orders with a partner set were While that commit improved performances (there are much less move lines to reconcile), it introduced an inconvenience business wise because of the noise created by the unreconciled lines This commit proposes to cut the pear in half by introducing an ir parameter to choose between the two options OPW 1883594 closes #26971 --- addons/point_of_sale/models/pos_order.py | 6 ++++++ addons/point_of_sale/models/pos_session.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index fcb8cf25222..e43200dbc4d 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -407,6 +407,12 @@ class PosOrder(models.Model): # for debugging and support purposes _logger.exception('Reconciliation did not work for order %s', order.name) + def _filtered_for_reconciliation(self): + filter_states = ['invoiced', 'done'] + if self.env['ir.config_parameter'].get_param('point_of_sale.order_reconcile_mode', 'all') == 'partner_only': + return self.filtered(lambda order: order.state in filter_states and order.partner_id) + return self.filtered(lambda order: order.state in filter_states) + def _default_session(self): return self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)], limit=1) diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 4ff493ee377..15ce31b6876 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -29,7 +29,7 @@ class PosSession(models.Model): if order.state not in ('paid'): raise UserError(_("You cannot confirm all orders of this session, because they have not the 'paid' status")) order.action_pos_order_done() - orders_to_reconcile = session.order_ids.filtered(lambda order: order.state in ['invoiced', 'done'] and order.partner_id) + orders_to_reconcile = session.order_ids._filtered_for_reconciliation() orders_to_reconcile.sudo()._reconcile_payments() config_id = fields.Many2one( From 2e6c1aa5d16f4ede8d9d685336c59e0cac34d487 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Wed, 12 Sep 2018 16:13:32 +0200 Subject: [PATCH 07/31] [FIX] stock_account: return dropship - Set a product with real-time valuation - Create a SO, select the Drop Ship route - Validate the SO, the PO, and receive the picking => an accounting entry is created - Return the picking The reversed accounting entry is not created. This is because the return of a drop ship picking is not recognized as some kind of dropshipping. opw-1879117 --- addons/stock_account/models/stock.py | 23 +++++-- .../tests/test_stockvaluation.py | 66 ++++++++++++++----- 2 files changed, 65 insertions(+), 24 deletions(-) diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index 7d13da6d2cb..05d37f80cc9 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -212,6 +212,14 @@ class StockMove(models.Model): """ return self.location_id.usage == 'supplier' and self.location_dest_id.usage == 'customer' + def _is_dropshipped_returned(self): + """ Check if the move should be considered as a returned dropshipping move so that the cost + method will be able to apply the correct logic. + + :return: True if the move is a returned dropshipping one else False + """ + return self.location_id.usage == 'customer' and self.location_dest_id.usage == 'supplier' + @api.model def _run_fifo(self, move, quantity=None): """ Value `move` according to the FIFO rule, meaning we consume the @@ -323,7 +331,7 @@ class StockMove(models.Model): 'value': value if quantity is None else self.value + value, 'price_unit': value / valued_quantity, }) - elif self._is_dropshipped(): + elif self._is_dropshipped() or self._is_dropshipped_returned(): curr_rounding = self.company_id.currency_id.rounding if self.product_id.cost_method in ['fifo']: price_unit = self._get_price_unit() @@ -335,8 +343,8 @@ class StockMove(models.Model): # In move have a positive value, out move have a negative value, let's arbitrary say # dropship are positive. self.write({ - 'value': value, - 'price_unit': price_unit, + 'value': value if self._is_dropshipped() else -value, + 'price_unit': price_unit if self._is_dropshipped() else -price_unit, }) def _action_done(self): @@ -359,7 +367,7 @@ class StockMove(models.Model): if company_src and company_dst and company_src.id != company_dst.id: raise UserError(_("The move lines are not in a consistent states: they are doing an intercompany in a single step while they should go through the intercompany transit location.")) move._run_valuation() - for move in res.filtered(lambda m: m.product_id.valuation == 'real_time' and (m._is_in() or m._is_out() or m._is_dropshipped())): + for move in res.filtered(lambda m: m.product_id.valuation == 'real_time' and (m._is_in() or m._is_out() or m._is_dropshipped() or m._is_dropshipped_returned())): move._account_entry_move() return res @@ -642,10 +650,13 @@ class StockMove(models.Model): else: self.with_context(force_company=company_from.id)._create_account_move_line(acc_valuation, acc_dest, journal_id) - if self.company_id.anglo_saxon_accounting and self._is_dropshipped(): + if self.company_id.anglo_saxon_accounting: # Creates an account entry from stock_input to stock_output on a dropship move. https://github.com/odoo/odoo/issues/12687 journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation() - self.with_context(force_company=self.company_id.id)._create_account_move_line(acc_src, acc_dest, journal_id) + if self._is_dropshipped(): + self.with_context(force_company=self.company_id.id)._create_account_move_line(acc_src, acc_dest, journal_id) + elif self._is_dropshipped_returned(): + self.with_context(force_company=self.company_id.id)._create_account_move_line(acc_dest, acc_src, journal_id) class StockReturnPicking(models.TransientModel): diff --git a/addons/stock_dropshipping/tests/test_stockvaluation.py b/addons/stock_dropshipping/tests/test_stockvaluation.py index 5dc475e056a..dac29bca7d4 100644 --- a/addons/stock_dropshipping/tests/test_stockvaluation.py +++ b/addons/stock_dropshipping/tests/test_stockvaluation.py @@ -36,7 +36,7 @@ class TestStockValuation(AccountingTestCase): # sell one unit of this product customer1 = self.env['res.partner'].create({'name': 'customer1'}) - sale_order1 = self.env['sale.order'].create({ + self.sale_order1 = self.env['sale.order'].create({ 'partner_id': customer1.id, 'partner_invoice_id': customer1.id, 'partner_shipping_id': customer1.id, @@ -50,38 +50,38 @@ class TestStockValuation(AccountingTestCase): 'pricelist_id': self.env.ref('product.list0').id, 'picking_policy': 'direct', }) - sale_order1.action_confirm() + self.sale_order1.action_confirm() # confirm the purchase order - purchase_order1 = self.env['purchase.order'].search([('group_id', '=', sale_order1.procurement_group_id.id)]) - purchase_order1.button_confirm() + self.purchase_order1 = self.env['purchase.order'].search([('group_id', '=', self.sale_order1.procurement_group_id.id)]) + self.purchase_order1.button_confirm() # validate the dropshipping picking - self.assertEqual(len(sale_order1.picking_ids), 1) - self.assertEqual(sale_order1.picking_ids.move_lines._is_dropshipped(), True) - wizard = sale_order1.picking_ids.button_validate() + self.assertEqual(len(self.sale_order1.picking_ids), 1) + self.assertEqual(self.sale_order1.picking_ids.move_lines._is_dropshipped(), True) + wizard = self.sale_order1.picking_ids.button_validate() immediate_transfer = self.env[wizard['res_model']].browse(wizard['res_id']) immediate_transfer.process() - self.assertEqual(sale_order1.picking_ids.state, 'done') + self.assertEqual(self.sale_order1.picking_ids.state, 'done') # create the vendor bill - vendor_bill1 = self.env['account.invoice'].create({ + self.vendor_bill1 = self.env['account.invoice'].create({ 'partner_id': vendor1.id, - 'purchase_id': purchase_order1.id, + 'purchase_id': self.purchase_order1.id, 'account_id': vendor1.property_account_payable_id.id, 'type': 'in_invoice', }) - vendor_bill1.purchase_order_change() - vendor_bill1.action_invoice_open() + self.vendor_bill1.purchase_order_change() + self.vendor_bill1.action_invoice_open() # create the customer invoice - customer_invoice1_id = sale_order1.action_invoice_create() - customer_invoice1 = self.env['account.invoice'].browse(customer_invoice1_id) - customer_invoice1.action_invoice_open() + self.customer_invoice1_id = self.sale_order1.action_invoice_create() + self.customer_invoice1 = self.env['account.invoice'].browse(self.customer_invoice1_id) + self.customer_invoice1.action_invoice_open() - all_amls = vendor_bill1.move_id.line_ids + customer_invoice1.move_id.line_ids - if sale_order1.picking_ids.move_lines.account_move_ids: - all_amls |= sale_order1.picking_ids.move_lines.account_move_ids.line_ids + all_amls = self.vendor_bill1.move_id.line_ids + self.customer_invoice1.move_id.line_ids + if self.sale_order1.picking_ids.move_lines.account_move_ids: + all_amls |= self.sale_order1.picking_ids.move_lines.account_move_ids.line_ids return all_amls def _check_results(self, expected_aml, expected_aml_count, all_amls): @@ -267,3 +267,33 @@ class TestStockValuation(AccountingTestCase): self._check_results(expected_aml, 8, all_amls) + def test_dropship_standard_perpetual_anglosaxon_ordered_return(self): + self.env.user.company_id.anglo_saxon_accounting = True + self.product1.product_tmpl_id.cost_method = 'standard' + self.product1.product_tmpl_id.standard_price = 10 + self.product1.product_tmpl_id.valuation = 'real_time' + self.product1.product_tmpl_id.invoice_policy = 'order' + + all_amls = self._dropship_product1() + + # return what we've done + stock_return_picking = self.env['stock.return.picking']\ + .with_context(active_ids=self.sale_order1.picking_ids.ids, active_id=self.sale_order1.picking_ids.ids[0])\ + .create({}) + stock_return_picking_action = stock_return_picking.create_returns() + return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id']) + return_pick.move_lines[0].move_line_ids[0].qty_done = 1.0 + return_pick.do_transfer() + self.assertEqual(return_pick.move_lines._is_dropshipped_returned(), True) + + all_amls_return = self.vendor_bill1.move_id.line_ids + self.customer_invoice1.move_id.line_ids + if self.sale_order1.picking_ids.mapped('move_lines.account_move_ids'): + all_amls_return |= self.sale_order1.picking_ids.mapped('move_lines.account_move_ids.line_ids') + + # Two extra AML should have been created for the return + expected_aml = { + self.acc_stock_in: (10.0, 0.0), + self.acc_stock_out: (0.0, 10.0), + } + + self._check_results(expected_aml, 2, all_amls_return - all_amls) From a47f7ee0e42604359bdafb90d09d3eba87b701dc Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Fri, 14 Sep 2018 10:13:20 +0200 Subject: [PATCH 08/31] [FIX] product: do not translate at code import Defined as this, the label of the selection field were translated during code import, when building the python model, not when a user access the field values as it should (and is already the case thanks to the ORM). Trying to translate a string when no user-context is available is not only useless but may cause bug on services with multiple databases (e.g. a SaaS). In a multi-worker environment, when the code is imported, the _ method will use multiple scenarios to detect the language and get a cursor. As we have no available cursor in the frame (method _get_cr from GettextAlias), the fallback is made on the cursor of the request. In a multi-worker environment, this could be a cursor linked to a database in another language than English. In such scenario, the selections would be translated in the language of the other database instead of displaying it in English opw-1881956 --- addons/barcodes/models/barcodes.py | 6 +++--- addons/l10n_mx/models/account.py | 2 +- addons/point_of_sale/models/barcode_rule.py | 10 +++++----- addons/pos_mercury/models/pos_mercury.py | 2 +- addons/product/models/product_template.py | 4 ++-- addons/project/models/project.py | 6 +++--- addons/stock/models/barcode.py | 8 ++++---- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/addons/barcodes/models/barcodes.py b/addons/barcodes/models/barcodes.py index 6c3d228895e..1ee93c3bdd6 100644 --- a/addons/barcodes/models/barcodes.py +++ b/addons/barcodes/models/barcodes.py @@ -174,14 +174,14 @@ class BarcodeRule(models.Model): barcode_nomenclature_id = fields.Many2one('barcode.nomenclature', string='Barcode Nomenclature') sequence = fields.Integer(string='Sequence', help='Used to order rules such that rules with a smaller sequence match first') encoding = fields.Selection([ - ('any', _('Any')), + ('any', 'Any'), ('ean13', 'EAN-13'), ('ean8', 'EAN-8'), ('upca', 'UPC-A'), ], string='Encoding', required=True, default='any', help='This rule will apply only if the barcode is encoded with the specified encoding') type = fields.Selection([ - ('alias', _('Alias')), - ('product', _('Unit Product')) + ('alias', 'Alias'), + ('product', 'Unit Product') ], string='Type', required=True, default='product') pattern = fields.Char(string='Barcode Pattern', size=32, help="The barcode matching pattern", required=True, default='.*') alias = fields.Char(string='Alias', size=32, default='0', help='The matched pattern will alias to this barcode', required=True) diff --git a/addons/l10n_mx/models/account.py b/addons/l10n_mx/models/account.py index b1c691c85f6..e7df50dbe0b 100644 --- a/addons/l10n_mx/models/account.py +++ b/addons/l10n_mx/models/account.py @@ -55,5 +55,5 @@ class AccountAccountTag(models.Model): _inherit = 'account.account.tag' nature = fields.Selection([ - ('D', _('Debitable Account')), ('A', _('Creditable Account'))], + ('D', 'Debitable Account'), ('A', 'Creditable Account')], help='Used in Mexican report of electronic accounting (account nature).') diff --git a/addons/point_of_sale/models/barcode_rule.py b/addons/point_of_sale/models/barcode_rule.py index 9fab9caa69a..5e505983006 100644 --- a/addons/point_of_sale/models/barcode_rule.py +++ b/addons/point_of_sale/models/barcode_rule.py @@ -9,9 +9,9 @@ class BarcodeRule(models.Model): _inherit = 'barcode.rule' type = fields.Selection(selection_add=[ - ('weight', _('Weighted Product')), - ('price', _('Priced Product')), - ('discount', _('Discounted Product')), - ('client', _('Client')), - ('cashier', _('Cashier')) + ('weight', 'Weighted Product'), + ('price', 'Priced Product'), + ('discount', 'Discounted Product'), + ('client', 'Client'), + ('cashier', 'Cashier') ]) diff --git a/addons/pos_mercury/models/pos_mercury.py b/addons/pos_mercury/models/pos_mercury.py index 0c48502ee76..659adca0969 100644 --- a/addons/pos_mercury/models/pos_mercury.py +++ b/addons/pos_mercury/models/pos_mercury.py @@ -13,7 +13,7 @@ class BarcodeRule(models.Model): _inherit = 'barcode.rule' type = fields.Selection(selection_add=[ - ('credit', _('Credit Card')) + ('credit', 'Credit Card') ]) diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 9056d40972b..8f27f14e59a 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -47,8 +47,8 @@ class ProductTemplate(models.Model): help="A description of the Product that you want to communicate to your customers. " "This description will be copied to every Sales Order, Delivery Order and Customer Invoice/Credit Note") type = fields.Selection([ - ('consu', _('Consumable')), - ('service', _('Service'))], string='Product Type', default='consu', required=True, + ('consu', 'Consumable'), + ('service', 'Service')], string='Product Type', default='consu', required=True, help='A stockable product is a product for which you manage stock. The "Inventory" app has to be installed.\n' 'A consumable product, on the other hand, is a product for which stock is not managed.\n' 'A service is a non-material product you provide.\n' diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 136477a4c1e..89fc9d5741c 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -197,9 +197,9 @@ class Project(models.Model): help="Internal email associated with this project. Incoming emails are automatically synchronized " "with Tasks (or optionally Issues if the Issue Tracker module is installed).") privacy_visibility = fields.Selection([ - ('followers', _('On invitation only')), - ('employees', _('Visible by all employees')), - ('portal', _('Visible by following customers')), + ('followers', 'On invitation only'), + ('employees', 'Visible by all employees'), + ('portal', 'Visible by following customers'), ], string='Privacy', required=True, default='employees', diff --git a/addons/stock/models/barcode.py b/addons/stock/models/barcode.py index 99b88475e47..7daf697f972 100644 --- a/addons/stock/models/barcode.py +++ b/addons/stock/models/barcode.py @@ -8,8 +8,8 @@ class BarcodeRule(models.Model): _inherit = 'barcode.rule' type = fields.Selection(selection_add=[ - ('weight', _('Weighted Product')), - ('location', _('Location')), - ('lot', _('Lot')), - ('package', _('Package')) + ('weight', 'Weighted Product'), + ('location', 'Location'), + ('lot', 'Lot'), + ('package', 'Package') ]) From a307d4fc80f7cf2090c03292bc86f6f063e695c0 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Fri, 7 Sep 2018 11:00:15 +0200 Subject: [PATCH 09/31] [FIX] account_asset: use the last day of fiscal year for depreciation date The depreciation was hardcoded as starting the first of January. If a user changed the configuration to set a different value than 31/12 it had no impact. In saas-11.3, this was improved to allow to configure the behaviour at 805f3a610 opw-1878927 --- addons/account_asset/models/account_asset.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/addons/account_asset/models/account_asset.py b/addons/account_asset/models/account_asset.py index afafa0e9c1f..bd423a806f8 100644 --- a/addons/account_asset/models/account_asset.py +++ b/addons/account_asset/models/account_asset.py @@ -217,7 +217,14 @@ class AccountAssetAsset(models.Model): # depreciation_date = 1st of January of purchase year if annual valuation, 1st of # purchase month in other cases if self.method_period >= 12: - asset_date = datetime.strptime(self.date[:4] + '-01-01', DF).date() + if self.company_id.fiscalyear_last_month: + asset_date = date(year=int(self.date[:4]), + month=self.company_id.fiscalyear_last_month, + day=self.company_id.fiscalyear_last_day) + \ + relativedelta(days=1) + \ + relativedelta(year=int(self.date[:4])) # e.g. 2018-12-31 +1 -> 2019 + else: + asset_date = datetime.strptime(self.date[:4] + '-01-01', DF).date() else: asset_date = datetime.strptime(self.date[:7] + '-01', DF).date() # if we already have some previous validated entries, starting date isn't 1st January but last entry + method period From 6e0afe864be15f9f59ac9d19be4ed7d451bec83e Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Fri, 14 Sep 2018 11:38:31 +0200 Subject: [PATCH 10/31] [FIX] website: AccessError The AccessError made sense when it was introduced back in v10, but doesn't make sense anymore since there is only one setting wizard. --- addons/website/models/res_config_settings.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/addons/website/models/res_config_settings.py b/addons/website/models/res_config_settings.py index 06b2c4118f3..ed339913ee9 100644 --- a/addons/website/models/res_config_settings.py +++ b/addons/website/models/res_config_settings.py @@ -80,8 +80,6 @@ class ResConfigSettings(models.TransientModel): return res def set_values(self): - if not self.user_has_groups('website.group_website_designer'): - raise AccessDenied() super(ResConfigSettings, self).set_values() set_param = self.env['ir.config_parameter'].sudo().set_param set_param('auth_signup.allow_uninvited', repr(self.auth_signup_uninvited == 'b2c')) From 8179960172d2c20c9cfdb74098676f60c9dd1e11 Mon Sep 17 00:00:00 2001 From: len-odoo Date: Mon, 3 Sep 2018 13:42:06 +0200 Subject: [PATCH 11/31] [FIX] web: lock handle when sorting on a different field The resequence mechanism works if the sequence is sorted by its handle field. Therefore sorting on another field should lock the handle, while sorting on the handle field should unlock the handle. opw 1867049 --- .../static/src/js/views/list/list_renderer.js | 3 +- addons/web/static/tests/views/list_tests.js | 70 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js index 2bb1dece89b..b65b93f7fdc 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -184,7 +184,6 @@ var ListRenderer = BasicRenderer.extend({ reject = columnInvisibleFields[c.attrs.name]; } if (!reject && c.attrs.widget === 'handle') { - self.hasHandle = true; self.handleField = c.attrs.name; } return reject; @@ -649,6 +648,8 @@ var ListRenderer = BasicRenderer.extend({ this._computeAggregates(); $table.toggleClass('o_list_view_grouped', is_grouped); $table.toggleClass('o_list_view_ungrouped', !is_grouped); + this.hasHandle = this.state.orderedBy.length === 0 || + this.state.orderedBy[0].name === this.handleField; if (is_grouped) { $table .append(this._renderHeader(true)) diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index eebf09c9365..9203cad8b99 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -3175,6 +3175,76 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('editable list, handle widget locks and unlocks on sort', function (assert) { + assert.expect(6); + + // we need another sortable field to lock/unlock the handle + this.data.foo.fields.amount.sortable = true; + // resequence makes sense on a sequence field, not on arbitrary fields + this.data.foo.records[0].int_field = 0; + this.data.foo.records[1].int_field = 1; + this.data.foo.records[2].int_field = 2; + this.data.foo.records[3].int_field = 3; + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '' + + '' + + '' + + '', + }); + + assert.strictEqual(list.$('tbody').text(), '1200.00500.00300.000.00', + "default should be sorted by int_field"); + + // Drag and drop the fourth line in second position + testUtils.dragAndDrop( + list.$('.ui-sortable-handle').eq(3), + list.$('tbody tr').first(), + {position: 'bottom'} + ); + + // Handle should be unlocked at this point + assert.strictEqual(list.$('tbody').text(), '1200.000.00500.00300.00', + "drag and drop should have succeeded, as the handle is unlocked"); + + // Sorting by a field different for int_field should lock the handle + list.$('.o_column_sortable').eq(1).click(); + + assert.strictEqual(list.$('tbody').text(), '0.00300.00500.001200.00', + "should have been sorted by amount"); + + // Drag and drop the fourth line in second position (not) + testUtils.dragAndDrop( + list.$('.ui-sortable-handle').eq(3), + list.$('tbody tr').first(), + {position: 'bottom'} + ); + + assert.strictEqual(list.$('tbody').text(), '0.00300.00500.001200.00', + "drag and drop should have failed as the handle is locked"); + + // Sorting by int_field should unlock the handle + list.$('.o_column_sortable').eq(0).click(); + + assert.strictEqual(list.$('tbody').text(), '1200.000.00500.00300.00', + "records should be ordered as per the previous resequence"); + + // Drag and drop the fourth line in second position + testUtils.dragAndDrop( + list.$('.ui-sortable-handle').eq(3), + list.$('tbody tr').first(), + {position: 'bottom'} + ); + + assert.strictEqual(list.$('tbody').text(), '1200.00300.000.00500.00', + "drag and drop should have worked as the handle is unlocked"); + + list.destroy(); + }); + QUnit.test('editable list with handle widget with slow network', function (assert) { assert.expect(15); From a87d5a2b28214195d02ddadcad0584e0fdd0cedd Mon Sep 17 00:00:00 2001 From: Moises Lopez Date: Thu, 16 Aug 2018 09:51:57 +0200 Subject: [PATCH 12/31] [ADD] sale: add menu to product categories It was already present in the accounting and purchase application but not in sale menus. Apply the same logic as in purchase and use a group_no_one Closes #22862 --- addons/sale/views/sale_views.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index 2d19e21b9c6..dd8104d6938 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -67,6 +67,11 @@ + From d39d583b213af02bad12721d9c610def6cb09161 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 14 Sep 2018 11:15:05 +0200 Subject: [PATCH 13/31] [FIX] product: partner property_pricelist load in batch for pos Reimplement the compute method of the field to make it work in batch. --- addons/product/models/product_pricelist.py | 53 +++++++++++++--------- addons/product/models/res_partner.py | 7 +-- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py index f38652785de..aa267ba5b7e 100644 --- a/addons/product/models/product_pricelist.py +++ b/addons/product/models/product_pricelist.py @@ -305,33 +305,42 @@ class Pricelist(models.Model): :param company_id: if passed, used for looking up properties, instead of current user's company """ + res = self._get_partner_pricelist_multi([partner_id], company_id) + return res[partner_id].id + + def _get_partner_pricelist_multi(self, partner_ids, company_id=None): + """ Retrieve the applicable pricelist for given partners in a given company. + + :param company_id: if passed, used for looking up properties, + instead of current user's company + :return: a dict {partner_id: pricelist} + """ Partner = self.env['res.partner'] Property = self.env['ir.property'].with_context(force_company=company_id or self.env.user.company_id.id) + Pricelist = self.env['product.pricelist'] - p = Partner.browse(partner_id) - pl = Property.get('property_product_pricelist', Partner._name, '%s,%s' % (Partner._name, p.id)) - if pl: - pl = pl[0].id + # retrieve values of property + result = Property.get_multi('property_product_pricelist', Partner._name, partner_ids) - if not pl: - if p.country_id.code: - pls = self.env['product.pricelist'].search([('country_group_ids.country_ids.code', '=', p.country_id.code)], limit=1) - pl = pls and pls[0].id + remaining_partner_ids = [pid for pid, val in result.items() if not val] + if remaining_partner_ids: + # get fallback pricelist when no pricelist for a given country + pl_fallback = ( + Pricelist.search([('country_group_ids', '=', False)], limit=1) or + Property.get('property_product_pricelist', 'res.partner') or + Pricelist.search([], limit=1) + ) + # group partners by country, and find a pricelist for each country + domain = [('id', 'in', remaining_partner_ids)] + groups = Partner.read_group(domain, ['country_id'], ['country_id']) + for group in groups: + country_id = group['country_id'] and group['country_id'][0] + pl = Pricelist.search([('country_group_ids.country_ids', '=', country_id)], limit=1) + pl = pl or pl_fallback + for pid in Partner.search(group['__domain']).ids: + result[pid] = pl - if not pl: - # search pl where no country - pls = self.env['product.pricelist'].search([('country_group_ids', '=', False)], limit=1) - pl = pls and pls[0].id - - if not pl: - prop = Property.get('property_product_pricelist', 'res.partner') - pl = prop and prop[0].id - - if not pl: - pls = self.env['product.pricelist'].search([], limit=1) - pl = pls and pls[0].id - - return pl + return result class ResCountryGroup(models.Model): diff --git a/addons/product/models/res_partner.py b/addons/product/models/res_partner.py index d307bd6ab09..bb3ead5e878 100644 --- a/addons/product/models/res_partner.py +++ b/addons/product/models/res_partner.py @@ -8,17 +8,18 @@ class Partner(models.Model): _name = 'res.partner' _inherit = 'res.partner' + # NOT A REAL PROPERTY !!!! property_product_pricelist = fields.Many2one( 'product.pricelist', 'Sale Pricelist', compute='_compute_product_pricelist', - inverse="_inverse_product_pricelist", company_dependent=False, # NOT A REAL PROPERTY + inverse="_inverse_product_pricelist", company_dependent=False, help="This pricelist will be used, instead of the default one, for sales to the current partner") @api.multi @api.depends('country_id') def _compute_product_pricelist(self): + res = self.env['product.pricelist']._get_partner_pricelist_multi(self.ids) for p in self: - if not isinstance(p.id, models.NewId): # if not onchange - p.property_product_pricelist = self.env['product.pricelist']._get_partner_pricelist(p.id) + p.property_product_pricelist = res.get(p.id) @api.one def _inverse_product_pricelist(self): From 70ed882088401c093e6a1180746b5637fe96da32 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 14 Sep 2018 11:15:40 +0200 Subject: [PATCH 14/31] [FIX] base: optimize the reading of many2one property fields --- odoo/addons/base/res/ir_property.py | 30 ++++++++++++++--- odoo/addons/test_new_api/models.py | 1 + .../test_new_api/tests/test_new_fields.py | 33 +++++++++++++++---- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/odoo/addons/base/res/ir_property.py b/odoo/addons/base/res/ir_property.py index 63ca6d2d6d0..4a7e0d7231b 100644 --- a/odoo/addons/base/res/ir_property.py +++ b/odoo/addons/base/res/ir_property.py @@ -159,11 +159,31 @@ class Property(models.Model): # note: order by 'company_id asc' will return non-null values first props = self.search(domain, order='company_id asc') result = {} - for prop in props: - # for a given res_id, take the first property only - id = refs.pop(prop.res_id, None) - if id is not None: - result[id] = prop.get_by_record() + + field = self.env[model]._fields[name] + if field.type == 'many2one': + # optimization for many2one fields + Comodel = self.env[field.comodel_name] + co_ids = set() + for prop in props: + # for a given res_id, take the first property only + id = refs.pop(prop.res_id, None) + if id is not None: + val = prop.value_reference + if val: + val = int(val.split(',')[1]) + co_ids.add(val) + result[id] = val + # check for existence in batch, and update result accordingly + existing = {rec.id: rec for rec in Comodel.browse(co_ids).exists()} + result = {id: existing.get(val, Comodel) for id, val in result.items()} + + else: + for prop in props: + # for a given res_id, take the first property only + id = refs.pop(prop.res_id, None) + if id is not None: + result[id] = prop.get_by_record() # set the default value to the ids that are not in result default_value = result.pop(False, False) diff --git a/odoo/addons/test_new_api/models.py b/odoo/addons/test_new_api/models.py index 2d973c04ef4..b50d92f5dde 100644 --- a/odoo/addons/test_new_api/models.py +++ b/odoo/addons/test_new_api/models.py @@ -349,6 +349,7 @@ class CompanyDependent(models.Model): _name = 'test_new_api.company' foo = fields.Char(company_dependent=True) + tag_id = fields.Many2one('test_new_api.multi.tag', company_dependent=True) class CompanyDependentAttribute(models.Model): _name = 'test_new_api.company.attr' diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index 8e54b6bb61a..52bda6b9ae3 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -564,6 +564,7 @@ class TestFields(common.TransactionCase): company0 = self.env.ref('base.main_company') company1 = self.env['res.company'].create({'name': 'A', 'parent_id': company0.id}) company2 = self.env['res.company'].create({'name': 'B', 'parent_id': company1.id}) + # create one user per company user0 = self.env['res.users'].create({'name': 'Foo', 'login': 'foo', 'company_id': company0.id, 'company_ids': []}) @@ -571,24 +572,44 @@ class TestFields(common.TransactionCase): 'company_id': company1.id, 'company_ids': []}) user2 = self.env['res.users'].create({'name': 'Baz', 'login': 'baz', 'company_id': company2.id, 'company_ids': []}) - # create a default value for the company-dependent field - field = self.env['ir.model.fields'].search([('model', '=', 'test_new_api.company'), - ('name', '=', 'foo')]) - self.env['ir.property'].create({'name': 'foo', 'fields_id': field.id, + + # create values for many2one field + tag0 = self.env['test_new_api.multi.tag'].create({'name': 'Qux'}) + tag1 = self.env['test_new_api.multi.tag'].create({'name': 'Quux'}) + tag2 = self.env['test_new_api.multi.tag'].create({'name': 'Quuz'}) + + # create default values for the company-dependent fields + field_foo = self.env['ir.model.fields']._get('test_new_api.company', 'foo') + self.env['ir.property'].create({'name': 'foo', 'fields_id': field_foo.id, 'value': 'default', 'type': 'char'}) + field_tag_id = self.env['ir.model.fields']._get('test_new_api.company', 'tag_id') + self.env['ir.property'].create({'name': 'foo', 'fields_id': field_tag_id.id, + 'value': tag0, 'type': 'many2one'}) # create/modify a record, and check the value for each user - record = self.env['test_new_api.company'].create({'foo': 'main'}) + record = self.env['test_new_api.company'].create({'foo': 'main', 'tag_id': tag1}) record.invalidate_cache() self.assertEqual(record.sudo(user0).foo, 'main') self.assertEqual(record.sudo(user1).foo, 'default') self.assertEqual(record.sudo(user2).foo, 'default') + self.assertEqual(record.sudo(user0).tag_id, tag1) + self.assertEqual(record.sudo(user1).tag_id, tag0) + self.assertEqual(record.sudo(user2).tag_id, tag0) - record.sudo(user1).foo = 'alpha' + record.sudo(user1).write({'foo': 'alpha', 'tag_id': tag2.id}) record.invalidate_cache() self.assertEqual(record.sudo(user0).foo, 'main') self.assertEqual(record.sudo(user1).foo, 'alpha') self.assertEqual(record.sudo(user2).foo, 'default') + self.assertEqual(record.sudo(user0).tag_id, tag1) + self.assertEqual(record.sudo(user1).tag_id, tag2) + self.assertEqual(record.sudo(user2).tag_id, tag0) + + # unlink value of a many2one (tag2), and check again + tag2.unlink() + self.assertEqual(record.sudo(user0).tag_id, tag1) + self.assertEqual(record.sudo(user1).tag_id, tag0.browse()) + self.assertEqual(record.sudo(user2).tag_id, tag0) # create company record and attribute company_record = self.env['test_new_api.company'].create({'foo': 'ABC'}) From b83db271354ee8ecabf4a39c96f3a187dd5535d5 Mon Sep 17 00:00:00 2001 From: MOENS Alexandre Date: Tue, 21 Aug 2018 12:00:49 +0200 Subject: [PATCH 15/31] [FIX] web: JournalDashboardGraph widget crash Mk.II Hardening of commit:995610c065bf0242cb5023cfad2940234395739e The JournalDashboardGraph requires nv, which is lazyloaded. However it requires nvd3.js which is loaded after nv.d3.js. A function called in destroy is defined by nv.d3.js, which can make the crashhappen with the right (wrong) timing. opw 1873749 --- .../web/static/src/js/fields/basic_fields.js | 2 +- .../static/tests/fields/basic_fields_tests.js | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index d624651ac85..71aab9f7992 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -2055,7 +2055,7 @@ var JournalDashboardGraph = AbstractField.extend({ return this._super.apply(this, arguments); }, destroy: function () { - if ('nv' in window) { + if ('nv' in window && nv.utils && nv.utils.offWindowResize) { // if the widget is destroyed before the lazy loaded libs (nv) are // actually loaded (i.e. after the widget has actually started), // nv is undefined, but the handler isn't bound yet anyway diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index fade0282f69..1133bed08b5 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -1902,6 +1902,45 @@ QUnit.module('basic_fields', { assert.verifySteps(['destroy']); }); + QUnit.test('graph dashboard widget can be destroyed when nv is partially loaded', function (assert) { + // this test ensures that the JournalDashboardGraph widget doesn't crash + // when being destroyed before nv has been completely loaded + assert.expect(2); + + testUtils.patch(basicFields.JournalDashboardGraph, { + destroy: function () { + assert.step('destroy'); + // nv is fully loaded only when nvd3.js has been loaded + // which happens sequentially after nv.d3.js + // we simulate this race condition with: + var offWindowResize = window.nv.utils.offWindowResize; + window.nv.utils.offWindowResize = undefined; + this._super.apply(this, arguments); + window.nv.utils.offWindowResize = offWindowResize; + }, + }); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '' + + '
' + + '' + + '
' + + '
' + + '
', + domain: [['id', 'in', [1]]], + }); + + kanban.destroy(); + testUtils.unpatch(basicFields.JournalDashboardGraph); + + assert.verifySteps(['destroy']); + }); + QUnit.module('AceEditor'); QUnit.test('ace widget on text fields works', function (assert) { From 14a9b988f43c01f9726bff68697ba492c5e26efd Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Mon, 20 Aug 2018 15:00:03 +0200 Subject: [PATCH 16/31] [FIX] web: graph_renderer: check graph data before rendering cherry pick of 634de21b76b9 Before this commit, we had two different errors when data was unavailable: The first one is generated by Odoo: "No data to display, try to[...]" The second one is generated by nvd3: "No Data Available". We always want the same message. So now, we always make some checks before rendering the graph by calling _render. opw-1874349 --- addons/web/static/src/js/views/graph/graph_renderer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js index 2b193bbdccb..9c7fc2081f1 100644 --- a/addons/web/static/src/js/views/graph/graph_renderer.js +++ b/addons/web/static/src/js/views/graph/graph_renderer.js @@ -57,7 +57,7 @@ return AbstractRenderer.extend({ on_attach_callback: function () { this._super.apply(this, arguments); this.isInDOM = true; - this._renderGraph(); + this._render(); }, /** * @override From 5eae8f7110b9663084b2e83dae201b6eada48fd5 Mon Sep 17 00:00:00 2001 From: RomainLibert Date: Fri, 14 Sep 2018 14:59:32 +0200 Subject: [PATCH 17/31] [FIX] crm,sale_crm: split action_mark_won Since https://github.com/odoo/odoo/commit/4330ea02676a4afd88707bb463a007e7de3c8686, action_mark_won was not api.multi anymore, which made it impossible to mark leads as won in batch. The problem is that we do exactly this in the demo data. As we need to keep the rainbowman in the interface the decision has been taken to split the method in two parts, one for the interface which returns the rainbowman (and uses ensure_one) and the second which is a true api.multi method. Thanks to @tde-banana-odoo for his advice on the matter --- addons/crm/models/crm_lead.py | 94 +++++++++++++----------- addons/crm/views/crm_lead_views.xml | 2 +- addons/sale_crm/views/crm_lead_views.xml | 2 +- 3 files changed, 53 insertions(+), 45 deletions(-) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 2ec7aa5f0bf..0e24634a568 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -363,52 +363,60 @@ class Lead(models.Model): for lead in self: stage_id = lead._stage_find(domain=[('probability', '=', 100.0), ('on_change', '=', True)]) lead.write({'stage_id': stage_id.id, 'probability': 100}) - if lead.user_id and lead.team_id and lead.planned_revenue: - query = """ - SELECT - SUM(CASE WHEN user_id = %(user_id)s THEN 1 ELSE 0 END) as total_won, - MAX(CASE WHEN date_closed >= CURRENT_DATE - INTERVAL '30 days' AND user_id = %(user_id)s THEN planned_revenue ELSE 0 END) as max_user_30, - MAX(CASE WHEN date_closed >= CURRENT_DATE - INTERVAL '7 days' AND user_id = %(user_id)s THEN planned_revenue ELSE 0 END) as max_user_7, - MAX(CASE WHEN date_closed >= CURRENT_DATE - INTERVAL '30 days' AND team_id = %(team_id)s THEN planned_revenue ELSE 0 END) as max_team_30, - MAX(CASE WHEN date_closed >= CURRENT_DATE - INTERVAL '7 days' AND team_id = %(team_id)s THEN planned_revenue ELSE 0 END) as max_team_7 - FROM crm_lead - WHERE - type = 'opportunity' - AND - active = True - AND - probability = 100 - AND - DATE_TRUNC('year', date_closed) = DATE_TRUNC('year', CURRENT_DATE) - AND - (user_id = %(user_id)s OR team_id = %(team_id)s) - """ - lead.env.cr.execute(query, {'user_id': lead.user_id.id, - 'team_id': lead.team_id.id}) - query_result = self.env.cr.dictfetchone() - message = False - if query_result['total_won'] == 1: - message = _('Go, go, go! Congrats for your first deal.') - elif query_result['max_team_30'] == lead.planned_revenue: - message = _('Boom! Team record for the past 30 days.') - elif query_result['max_team_7'] == lead.planned_revenue: - message = _('Yeah! Deal of the last 7 days for the team.') - elif query_result['max_user_30'] == lead.planned_revenue: - message = _('You just beat your personal record for the past 30 days.') - elif query_result['max_user_7'] == lead.planned_revenue: - message = _('You just beat your personal record for the past 7 days.') + return True - if message: - return { - 'effect': { - 'fadeout': 'slow', - 'message': message, - 'img_url': '/web/image/%s/%s/image' % (lead.team_id.user_id._name, lead.team_id.user_id.id) if lead.team_id.user_id.image else '/web/static/src/img/smile.svg', - 'type': 'rainbow_man', - } + @api.multi + def action_set_won_rainbowman(self): + self.ensure_one() + self.action_set_won() + + if self.user_id and self.team_id and self.planned_revenue: + query = """ + SELECT + SUM(CASE WHEN user_id = %(user_id)s THEN 1 ELSE 0 END) as total_won, + MAX(CASE WHEN date_closed >= CURRENT_DATE - INTERVAL '30 days' AND user_id = %(user_id)s THEN planned_revenue ELSE 0 END) as max_user_30, + MAX(CASE WHEN date_closed >= CURRENT_DATE - INTERVAL '7 days' AND user_id = %(user_id)s THEN planned_revenue ELSE 0 END) as max_user_7, + MAX(CASE WHEN date_closed >= CURRENT_DATE - INTERVAL '30 days' AND team_id = %(team_id)s THEN planned_revenue ELSE 0 END) as max_team_30, + MAX(CASE WHEN date_closed >= CURRENT_DATE - INTERVAL '7 days' AND team_id = %(team_id)s THEN planned_revenue ELSE 0 END) as max_team_7 + FROM crm_lead + WHERE + type = 'opportunity' + AND + active = True + AND + probability = 100 + AND + DATE_TRUNC('year', date_closed) = DATE_TRUNC('year', CURRENT_DATE) + AND + (user_id = %(user_id)s OR team_id = %(team_id)s) + """ + self.env.cr.execute(query, {'user_id': self.user_id.id, + 'team_id': self.team_id.id}) + query_result = self.env.cr.dictfetchone() + + message = False + if query_result['total_won'] == 1: + message = _('Go, go, go! Congrats for your first deal.') + elif query_result['max_team_30'] == self.planned_revenue: + message = _('Boom! Team record for the past 30 days.') + elif query_result['max_team_7'] == self.planned_revenue: + message = _('Yeah! Deal of the last 7 days for the team.') + elif query_result['max_user_30'] == self.planned_revenue: + message = _('You just beat your personal record for the past 30 days.') + elif query_result['max_user_7'] == self.planned_revenue: + message = _('You just beat your personal record for the past 7 days.') + + if message: + return { + 'effect': { + 'fadeout': 'slow', + 'message': message, + 'img_url': '/web/image/%s/%s/image' % (self.team_id.user_id._name, self.team_id.user_id.id) if self.team_id.user_id.image else '/web/static/src/img/smile.svg', + 'type': 'rainbow_man', } - return True + } + return True @api.multi def action_schedule_meeting(self): diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index c3384a4bc96..0a5dfc1e9c5 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -430,7 +430,7 @@
-