From d28363ba5b779bf7aa0745d47d8cdc6e7c13afc7 Mon Sep 17 00:00:00 2001 From: rhe-odoo Date: Fri, 28 Apr 2023 11:24:18 +0000 Subject: [PATCH] [FIX] pos_sale: correctly assign partners in the invoice When settling a sale.order from the pos app, it's possible that the sale.order have different partners assigned to it (partner_id, partner_shipping_id, partner_invoice_id). After this commit, we are now properly assigning these partners to the generated invoice. Additionally, this commit makes the settling of sale.order more robust by avoiding issue on having multiple incompatible sale.orders in a pos.order (more than 2 sale.orders and their partners are not the same). After this commit, we are only reusing the current pos.order as container of the new sale.order to settle if the sale.order is compatible to the current pos.order. Basically, we now only allow adding new sale order to the current order if the partner_id, partner_invoice_id and partner_shipping_id of the new sale.order are the same to the current pos.order's linked sale.order. In other words, if the new sale.order to settle is incompatible to the current pos.order, we create a new pos.order to settle that sale.order. closes odoo/odoo#120649 X-original-commit: 2782bedafb9fcbe7c65f4c6951ce270c3a5a5bd3 Signed-off-by: Heinz Robin (rhe) Signed-off-by: Joseph Caburnay (jcb) --- addons/pos_sale/models/pos_order.py | 17 +++++---- .../SaleOrderManagementScreen.js | 38 ++++++++++++++++++- .../tests/helpers/ProductScreenTourMethods.js | 12 ++++++ .../static/tests/tours/PosSaleTour.js | 19 ++++++++++ addons/pos_sale/tests/test_pos_sale_flow.py | 32 ++++++++++++++++ 5 files changed, 110 insertions(+), 8 deletions(-) diff --git a/addons/pos_sale/models/pos_order.py b/addons/pos_sale/models/pos_order.py index 3a8300834c6..3378b9ffeeb 100644 --- a/addons/pos_sale/models/pos_order.py +++ b/addons/pos_sale/models/pos_order.py @@ -32,13 +32,16 @@ class PosOrder(models.Model): invoice_vals = super(PosOrder, self)._prepare_invoice_vals() invoice_vals['team_id'] = self.crm_team_id.id sale_orders = self.lines.mapped('sale_order_origin_id') - if sale_orders and (sale_orders.partner_invoice_id.id != sale_orders.partner_shipping_id.id): - invoice_vals['partner_shipping_id'] = sale_orders.partner_shipping_id.id - else: - addr = self.partner_id.address_get(['delivery']) - invoice_vals['partner_shipping_id'] = addr['delivery'] - if sale_orders and sale_orders[0].payment_term_id: - invoice_vals['invoice_payment_term_id'] = sale_orders[0].payment_term_id.id, + if sale_orders: + if sale_orders[0].partner_invoice_id.id != sale_orders[0].partner_shipping_id.id: + invoice_vals['partner_shipping_id'] = sale_orders[0].partner_shipping_id.id + else: + addr = self.partner_id.address_get(['delivery']) + invoice_vals['partner_shipping_id'] = addr['delivery'] + if sale_orders[0].payment_term_id: + invoice_vals['invoice_payment_term_id'] = sale_orders[0].payment_term_id.id + if sale_orders[0].partner_invoice_id != sale_orders[0].partner_id: + invoice_vals['partner_id'] = sale_orders[0].partner_invoice_id.id return invoice_vals @api.model diff --git a/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderManagementScreen.js b/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderManagementScreen.js index d95413aeedd..c25fe940e73 100644 --- a/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderManagementScreen.js +++ b/addons/pos_sale/static/src/js/OrderManagementScreen/SaleOrderManagementScreen.js @@ -18,6 +18,15 @@ import { SaleOrderList } from "./SaleOrderList"; import { SaleOrderManagementControlPanel } from "./SaleOrderManagementControlPanel"; import { onMounted, useRef, useState } from "@odoo/owl"; +/** + * ID getter to take into account falsy many2one value. + * @param {[id: number, display_name: string] | false} fieldVal many2one field value + * @returns {number | false} + */ +function getId(fieldVal) { + return fieldVal && fieldVal[0]; +} + export class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentToOrderScreen) { static components = { SaleOrderList, SaleOrderManagementControlPanel }; static template = "SaleOrderManagementScreen"; @@ -29,6 +38,7 @@ export class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentTo this.root = useRef("root"); this.numberBuffer = useService("number_buffer"); this.saleOrderFetcher = useService("sale_order_fetcher"); + this.notification = useService("pos_notification"); useBus(this.saleOrderFetcher, "update", this.render); this.orderManagementContext = useState(orderManagement); @@ -48,6 +58,14 @@ export class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentTo ); this.saleOrderFetcher.setNPerPage(val); this.saleOrderFetcher.fetch(); + } + _getSaleOrderOrigin(order) { + for (const line of order.get_orderlines()) { + if (line.sale_order_origin_id) { + return line.sale_order_origin_id + } + } + return false; } get selectedPartner() { const order = this.orderManagementContext.selectedOrder; @@ -82,9 +100,25 @@ export class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentTo }); if (confirmed) { - const currentPOSOrder = this.env.pos.get_order(); + let currentPOSOrder = this.env.pos.get_order(); const sale_order = await this._getSaleOrder(clickedOrder.id); clickedOrder.shipping_date = sale_order.shipping_date; + + const currentSaleOrigin = this._getSaleOrderOrigin(currentPOSOrder); + const currentSaleOriginId = currentSaleOrigin && currentSaleOrigin.id; + + if (currentSaleOriginId) { + const linkedSO = await this._getSaleOrder(currentSaleOriginId); + if ( + getId(linkedSO.partner_id) !== getId(sale_order.partner_id) || + getId(linkedSO.partner_invoice_id) !== getId(sale_order.partner_invoice_id) || + getId(linkedSO.partner_shipping_id) !== getId(sale_order.partner_shipping_id) + ) { + currentPOSOrder = this.env.pos.add_new_order(); + this.notification.add(this.env._t("A new order has been created."), 4000); + } + } + try { await this.env.pos.load_new_partners(); } catch { @@ -318,6 +352,8 @@ export class SaleOrderManagementScreen extends ControlButtonsMixin(IndependentTo "amount_untaxed", "amount_unpaid", "picking_ids", + "partner_shipping_id", + "partner_invoice_id" ] ); diff --git a/addons/pos_sale/static/tests/helpers/ProductScreenTourMethods.js b/addons/pos_sale/static/tests/helpers/ProductScreenTourMethods.js index 0f7b2dd9b46..63791a0c3ba 100644 --- a/addons/pos_sale/static/tests/helpers/ProductScreenTourMethods.js +++ b/addons/pos_sale/static/tests/helpers/ProductScreenTourMethods.js @@ -25,6 +25,18 @@ class DoExt extends Do { } ]; } + selectNthOrder(n) { + return [ + { + content: `select order`, + trigger: `.order-list .order-row:nth-child(${n})`, + }, + { + content: `click on select the order`, + trigger: `.selection-item:contains('Settle the order')`, + } + ]; + } } class CheckExt extends Check {} diff --git a/addons/pos_sale/static/tests/tours/PosSaleTour.js b/addons/pos_sale/static/tests/tours/PosSaleTour.js index 74ff427b08a..0b42116be3c 100644 --- a/addons/pos_sale/static/tests/tours/PosSaleTour.js +++ b/addons/pos_sale/static/tests/tours/PosSaleTour.js @@ -25,3 +25,22 @@ Chrome.do.clickMenuButton(); Chrome.do.clickTicketButton(); registry.category("web_tour.tours").add('PosSettleOrder', { test: true, url: '/pos/ui', steps: getSteps() }); + +startSteps(); + +ProductScreen.do.confirmOpeningPopup(); +ProductScreen.do.clickQuotationButton(); +// The second item in the list is the first sale.order. +ProductScreen.do.selectNthOrder(2); +ProductScreen.check.selectedOrderlineHas('product1', 1); +ProductScreen.check.totalAmountIs("10.00"); + +ProductScreen.do.clickQuotationButton(); +// The first item in the list is the second sale.order. +// Selecting the 2nd sale.order should use a new order, +// therefore, the total amount will change. +ProductScreen.do.selectNthOrder(1); +ProductScreen.check.selectedOrderlineHas('product2', 1); +ProductScreen.check.totalAmountIs("11.00"); + +registry.category("web_tour.tours").add('PosSettleOrderIncompatiblePartner', { test: true, url: '/pos/ui', steps: getSteps() }); diff --git a/addons/pos_sale/tests/test_pos_sale_flow.py b/addons/pos_sale/tests/test_pos_sale_flow.py index 9c9a7225510..8c416441663 100644 --- a/addons/pos_sale/tests/test_pos_sale_flow.py +++ b/addons/pos_sale/tests/test_pos_sale_flow.py @@ -70,3 +70,35 @@ class TestPoSSale(TestPointOfSaleHttpCommon): self.assertEqual(sale_order.picking_ids[0].move_ids.quantity_done, 0) self.assertEqual(sale_order.picking_ids[1].move_ids.product_qty, 300) self.assertEqual(sale_order.picking_ids[1].move_ids.quantity_done, 300) # 1 delivered => 300 * 2 = 600 + + def test_settle_order_with_incompatible_partner(self): + """ If the partner of the sale order is not compatible with the current pos order, + then a new pos order should be to settle the newly selected sale order. + """ + + product1 = self.env['product.product'].create({ + 'name': 'product1', + 'available_in_pos': True, + 'type': 'product', + 'lst_price': 10, + 'taxes_id': [odoo.Command.clear()], + }) + product2 = self.env['product.product'].create({ + 'name': 'product2', + 'available_in_pos': True, + 'type': 'product', + 'lst_price': 11, + 'taxes_id': [odoo.Command.clear()], + }) + self.env['sale.order'].create({ + 'partner_id': self.env.ref('base.res_partner_1').id, + 'partner_shipping_id': self.env.ref('base.res_partner_2').id, + 'order_line': [(0, 0, {'product_id': product1.id})], + }) + self.env['sale.order'].create({ + 'partner_id': self.env.ref('base.res_partner_1').id, + 'partner_shipping_id': self.env.ref('base.res_partner_1').id, + 'order_line': [(0, 0, {'product_id': product2.id})], + }) + self.main_pos_config.open_ui() + self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'PosSettleOrderIncompatiblePartner', login="accountman")