From 65c23b4bc75f0b30f8b24fd5ddbf50ea63c65ad9 Mon Sep 17 00:00:00 2001 From: roen-odoo Date: Fri, 30 Dec 2022 10:36:41 +0000 Subject: [PATCH] [FIX] point_of_sale: rounding of refund orders Current behavior: When a refund order is created, the amount is not rounded the same way as the orginial order. Steps to reproduce: - Create a rouding method with 0.05 as rounding value, with the DOWN rounding method - Create an order with a product of price 1.98 - Validate the order, the price should be 1.95 - Create a refund order for the previous order - Validate the refund order, the price should be -1.95 but it's -2.00 Solution: Always act like the order has a positive amount, and apply the rounding the normal way. Then apply the sign of the original order. If the remaining amount has a different sign than the original total, we should invert the rounding method to apply a rounding in the same "direction" as the original order. For example : - Original order is -1.98 - Rounding is applied and the total is -2.00 - After the payment, the remaining is -1.98 - -2.00 = 0.02 - Wich will be rounded to 0.05 wich is not correct, it should be rounded to 0.00. So we invert the rounding method to apply a rounding in the same "direction" as the original order. If we don't do this the total due would have been -1.95 instead of -2.00. And the change would have been 0.05 instead of 0.00. opw-3106656 closes odoo/odoo#109422 X-original-commit: c92074e8ec5b4a11fa90f08a16c859bb72234863 Signed-off-by: Heinz Robin (rhe) Signed-off-by: Engels Robin (roen) --- addons/point_of_sale/static/src/js/models.js | 38 ++++++------- .../static/tests/tours/PaymentScreen.tour.js | 54 +++++++++++++++++++ .../tours/helpers/PaymentScreenTourMethods.js | 9 ++++ addons/point_of_sale/tests/test_frontend.py | 44 +++++++++++++++ 4 files changed, 122 insertions(+), 23 deletions(-) diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 640dcc8f5e4..25db2ee6fde 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -3394,12 +3394,17 @@ export class Order extends PosModel { ? last_line.payment_method.is_cash_count == true : false; if (!only_cash || (only_cash && last_line_is_cash)) { + var rounding_method = this.pos.cash_rounding[0].rounding_method; var remaining = this.get_total_with_tax() - this.get_total_paid(); - var total = round_pr(remaining, this.pos.cash_rounding[0].rounding); - var sign = remaining > 0 ? 1.0 : -1.0; + var sign = this.get_total_with_tax() > 0 ? 1.0 : -1.0; + if(this.get_total_with_tax() < 0 && remaining > 0 || this.get_total_with_tax() > 0 && remaining < 0) { + rounding_method = rounding_method === "UP" ? "DOWN" : "UP"; + } + remaining *= sign; + var total = round_pr(remaining, this.pos.cash_rounding[0].rounding); var rounding_applied = total - remaining; - rounding_applied *= sign; + // because floor and ceil doesn't include decimals in calculation, we reuse the value of the half-up and adapt it. if (utils.float_is_zero(rounding_applied, this.pos.currency.decimal_places)) { // https://xkcd.com/217/ @@ -3408,29 +3413,16 @@ export class Order extends PosModel { Math.abs(this.get_total_with_tax()) < this.pos.cash_rounding[0].rounding ) { return 0; - } else if ( - this.pos.cash_rounding[0].rounding_method === "UP" && - rounding_applied < 0 && - remaining > 0 - ) { + } else if(rounding_method === "UP" && rounding_applied < 0 && remaining > 0) { rounding_applied += this.pos.cash_rounding[0].rounding; - } else if ( - this.pos.cash_rounding[0].rounding_method === "UP" && - rounding_applied > 0 && - remaining < 0 - ) { + } + else if(rounding_method === "UP" && rounding_applied > 0 && remaining < 0) { rounding_applied -= this.pos.cash_rounding[0].rounding; - } else if ( - this.pos.cash_rounding[0].rounding_method === "DOWN" && - rounding_applied > 0 && - remaining > 0 - ) { + } + else if(rounding_method === "DOWN" && rounding_applied > 0 && remaining > 0){ rounding_applied -= this.pos.cash_rounding[0].rounding; - } else if ( - this.pos.cash_rounding[0].rounding_method === "DOWN" && - rounding_applied < 0 && - remaining < 0 - ) { + } + else if(rounding_method === "DOWN" && rounding_applied < 0 && remaining < 0){ rounding_applied += this.pos.cash_rounding[0].rounding; } return sign * rounding_applied; diff --git a/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js b/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js index 67e517d432d..0f3d12f9569 100644 --- a/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js +++ b/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js @@ -1,7 +1,9 @@ /** @odoo-module */ +import { Chrome } from "@point_of_sale/../tests/tours/helpers/ChromeTourMethods"; import { ProductScreen } from "@point_of_sale/../tests/tours/helpers/ProductScreenTourMethods"; import { PaymentScreen } from "@point_of_sale/../tests/tours/helpers/PaymentScreenTourMethods"; +import { TicketScreen} from "@point_of_sale/../tests/tours/helpers/TicketScreenTourMethods"; import { getSteps, startSteps } from "@point_of_sale/../tests/tours/helpers/utils"; import Tour from "web_tour.tour"; @@ -80,3 +82,55 @@ PaymentScreen.check.remainingIs("0.0"); PaymentScreen.check.changeIs("0.0"); Tour.register("PaymentScreenTour2", { test: true, url: "/pos/ui" }, getSteps()); + +startSteps(); + +ProductScreen.do.clickHomeCategory(); +ProductScreen.exec.addOrderline('Product Test', '1'); +ProductScreen.do.clickPayButton(); + +PaymentScreen.check.totalIs('2.00'); +PaymentScreen.do.clickPaymentMethod('Cash'); + +PaymentScreen.check.remainingIs('0.0'); +PaymentScreen.check.changeIs('0.0'); + +Chrome.do.clickTicketButton(); +TicketScreen.do.clickNewTicket(); + +ProductScreen.exec.addOrderline('Product Test', '-1'); +ProductScreen.do.clickPayButton(); + +PaymentScreen.check.totalIs('-2.00'); +PaymentScreen.do.clickPaymentMethod('Cash'); + +PaymentScreen.check.remainingIs('0.0'); +PaymentScreen.check.changeIs('0.0'); + +Tour.register('PaymentScreenRoundingUp', { test: true, url: '/pos/ui' }, getSteps()); + +startSteps(); + +ProductScreen.do.clickHomeCategory(); +ProductScreen.exec.addOrderline('Product Test', '1'); +ProductScreen.do.clickPayButton(); + +PaymentScreen.check.totalIs('1.95'); +PaymentScreen.do.clickPaymentMethod('Cash'); + +PaymentScreen.check.remainingIs('0.0'); +PaymentScreen.check.changeIs('0.0'); + +Chrome.do.clickTicketButton(); +TicketScreen.do.clickNewTicket(); + +ProductScreen.exec.addOrderline('Product Test', '-1'); +ProductScreen.do.clickPayButton(); + +PaymentScreen.check.totalIs('-1.95'); +PaymentScreen.do.clickPaymentMethod('Cash'); + +PaymentScreen.check.remainingIs('0.0'); +PaymentScreen.check.changeIs('0.0'); + +Tour.register('PaymentScreenRoundingDown', { test: true, url: '/pos/ui' }, getSteps()); diff --git a/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js index 98baeca8c61..81934b97d76 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js @@ -185,6 +185,15 @@ class Check { }, ]; } + totalIs(amount) { + return [ + { + content: `total is ${amount}`, + trigger: `.total:contains("${amount}")`, + run: () => {}, + }, + ]; + } } class Execute { diff --git a/addons/point_of_sale/tests/test_frontend.py b/addons/point_of_sale/tests/test_frontend.py index 0a1d737f7a9..d518022489c 100644 --- a/addons/point_of_sale/tests/test_frontend.py +++ b/addons/point_of_sale/tests/test_frontend.py @@ -590,3 +590,47 @@ class TestUi(TestPointOfSaleHttpCommon): self.main_pos_config.write({'payment_method_ids': [(6, 0, bank_pm.ids)]}) self.main_pos_config.open_ui() self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'PaymentScreenTour2', login="accountman") + + def test_rounding_up(self): + rouding_method = self.env['account.cash.rounding'].create({ + 'name': 'Rounding up', + 'rounding': 0.05, + 'rounding_method': 'UP', + }) + + self.env['product.product'].create({ + 'name': 'Product Test', + 'available_in_pos': True, + 'list_price': 1.98, + 'taxes_id': False, + }) + + self.main_pos_config.write({ + 'rounding_method': rouding_method.id, + 'cash_rounding': True, + }) + + self.main_pos_config.open_ui() + self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'PaymentScreenRoundingUp', login="accountman") + + def test_rounding_down(self): + rouding_method = self.env['account.cash.rounding'].create({ + 'name': 'Rounding down', + 'rounding': 0.05, + 'rounding_method': 'DOWN', + }) + + self.env['product.product'].create({ + 'name': 'Product Test', + 'available_in_pos': True, + 'list_price': 1.98, + 'taxes_id': False, + }) + + self.main_pos_config.write({ + 'rounding_method': rouding_method.id, + 'cash_rounding': True, + }) + + self.main_pos_config.open_ui() + self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'PaymentScreenRoundingDown', login="accountman")