From f7e6523bd7ea0f541aef2fef09e48d98aab00469 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Mon, 15 Apr 2024 14:59:02 +0200 Subject: [PATCH] [FIX] stock: prevent opening SM when picking is dirty Steps to reproduce ================== - Create two new storable products tracked by USN - Create a new RFQ with one of the created product - Confirm the order - Open the receipt - Add a new line with the other product - Click on the open move button in the new line - Add a new SN - Save & close => Cannot read properties of undefined (reading 'resId') Cause of the issue ================== When calling openRecord, if the record is dirty, it is saved before proceeding. After saving, we call super.openRecord with the old record. Since that record is no longer linked to the root record (the stock.picking), when we try to save it, it won't match an existing id. Solution ======== If the record is new, we don't save as there would be no way of knowing which of the returned line would come from this one. If we are opening an existing record, we find the new datapoint by matching it's ID. opw-3777615 closes odoo/odoo#162425 Signed-off-by: William Henrotin (whe) --- .../views/picking_form/stock_move_one2many.js | 6 +- .../static/tests/tours/stock_picking_tour.js | 52 ++++++++++++++++ addons/stock/tests/test_picking_tours.py | 61 +++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) diff --git a/addons/stock/static/src/views/picking_form/stock_move_one2many.js b/addons/stock/static/src/views/picking_form/stock_move_one2many.js index d618192bec6..a1db35eab0a 100644 --- a/addons/stock/static/src/views/picking_form/stock_move_one2many.js +++ b/addons/stock/static/src/views/picking_form/stock_move_one2many.js @@ -46,10 +46,14 @@ export class StockMoveX2ManyField extends X2ManyField { async openRecord(record) { - if (this.canOpenRecord) { + if (this.canOpenRecord && !record.isNew) { const dirty = await record.isDirty(); if (dirty && 'quantity' in record._changes) { await record.model.root.save({ reload: true }); + record = record.model.root.data[this.props.name].records.find(e => e.resId === record.resId); + if (!record) { + return; + } } } return super.openRecord(record); diff --git a/addons/stock/static/tests/tours/stock_picking_tour.js b/addons/stock/static/tests/tours/stock_picking_tour.js index 6b42ca4fffe..4361c7db37b 100644 --- a/addons/stock/static/tests/tours/stock_picking_tour.js +++ b/addons/stock/static/tests/tours/stock_picking_tour.js @@ -104,3 +104,55 @@ registry.category("web_tour.tours").add('test_generate_serial_1', { test: true, isCheck: true, }, ]}); + +registry.category("web_tour.tours").add('test_add_new_line', { + test: true, + steps: () => [ + { + extra_trigger: '.o_form_editable', + trigger: '.o_field_x2many_list_row_add > a' + }, + { + trigger: ".o_field_widget[name=product_id] input", + run: 'text two', + }, + { trigger: ".ui-menu-item > a:contains('Product two')" }, + { trigger: ".fa-list:eq(1)" }, + { trigger: "h4:contains('Stock move')" }, + { trigger: '.o_field_x2many_list_row_add > a' }, + { + trigger: ".o_field_widget[name=lot_name] input", + run: 'text two', + }, + { trigger: ".o_form_view.modal-content .o_form_button_save" }, + { trigger: ".o_form_view:not(.modal-content) .o_form_button_save" }, + { + trigger: ".o_form_renderer.o_form_saved", + isCheck: true, + }, + ] +}); + +registry.category("web_tour.tours").add('test_edit_existing_line', { + test: true, + steps: () => [ + { trigger: ".o_data_cell[name=quantity]" }, + { + trigger: ".o_field_widget[name=quantity] input", + run: 'text 2', + }, + { trigger: ".fa-list" }, + { trigger: "h4:contains('Stock move')" }, + { trigger: ".o_data_cell[name=quantity]:eq(1)" }, + { + trigger: ".o_field_widget[name=lot_name] input", + run: 'text two', + }, + { trigger: ".o_form_view.modal-content .o_form_button_save" }, + { trigger: ".o_form_view:not(.modal-content) .o_form_button_save" }, + { + trigger: ".o_form_renderer.o_form_saved", + isCheck: true, + }, + ] +}); diff --git a/addons/stock/tests/test_picking_tours.py b/addons/stock/tests/test_picking_tours.py index be95c7b60b8..8ddf6bc91c3 100644 --- a/addons/stock/tests/test_picking_tours.py +++ b/addons/stock/tests/test_picking_tours.py @@ -1,4 +1,5 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import Command from odoo.tests import HttpCase, tagged @@ -57,3 +58,63 @@ class TestStockPickingTour(HttpCase): ('product_id', '=', product_serial.id), ]) self.assertEqual(len(serial), 5) + + def test_add_new_line(self): + product_one, _ = self.env["product.product"].create([ + { + 'name': 'Product one', + 'type': 'product', + 'tracking': 'serial', + }, + { + 'name': 'Product two', + 'type': 'product', + 'tracking': 'serial', + } + ]) + + self.receipt.write({ + "move_ids": [Command.create({ + "name": "test", + "product_uom_qty": 1, + "product_id": product_one.id, + "location_id": self.receipt.location_id.id, + "location_dest_id": self.receipt.location_dest_id.id, + })] + }) + + self.receipt.action_confirm() + self.receipt.move_ids.move_line_ids.lot_name = "one" + + url = self._get_picking_url(self.receipt.id) + self.start_tour(url, 'test_add_new_line', login='admin', timeout=60) + + names = self.receipt.move_ids.move_line_ids.mapped('lot_name') + self.assertEqual(names, ["two", "one"]) + + def test_edit_existing_line(self): + self.uom_unit = self.env.ref('uom.product_uom_unit') + product_one = self.env['product.product'].create({ + 'name': 'Product one', + 'type': 'product', + 'tracking': 'serial', + }) + + self.receipt.write({ + "move_ids": [Command.create({ + "name": "test", + "product_id": product_one.id, + "location_id": self.receipt.location_id.id, + "location_dest_id": self.receipt.location_dest_id.id, + "product_uom_qty": 1, + })] + }) + + self.receipt.action_confirm() + self.receipt.move_ids.move_line_ids.lot_name = "one" + + url = self._get_picking_url(self.receipt.id) + self.start_tour(url, 'test_edit_existing_line', login='admin', timeout=60) + + names = self.receipt.move_ids.move_line_ids.mapped('lot_name') + self.assertEqual(names, ["one", "two"])