From eb5420da2cd5738aa1e4babe807dc96221bfa876 Mon Sep 17 00:00:00 2001 From: Robin Heinz Date: Tue, 20 Apr 2021 09:23:57 +0000 Subject: [PATCH] [FIX] pos_cash_rounding: fix when multiple payment method When a pos user combines payment methods with and without rounding, we must be sure that the rounding is made by payment method and not only on the total amount of the order. As the total may change depending on the combination of payment method used, we had to adapt the methods that checks that an order is paid, and also propagate the new rounding made on the invoice. We also don't allow anymore a user to validate an order with a payment badly rounded. closes odoo/odoo#69754 X-original-commit: 8cf38faec03d167a7509f0ad6e041a13e4f7ad53 Signed-off-by: pimodoo --- addons/point_of_sale/models/pos_order.py | 86 +++++++++++++++++-- .../js/Screens/PaymentScreen/PaymentScreen.js | 9 ++ addons/point_of_sale/static/src/js/models.js | 30 ++++++- .../Screens/ReceiptScreen/OrderReceipt.xml | 8 +- 4 files changed, 120 insertions(+), 13 deletions(-) diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index d30e3cc2cd7..87b1609f533 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -370,6 +370,70 @@ class PosOrder(models.Model): currency = self.currency_id return currency.round(amount) if currency else amount + def _create_invoice(self, move_vals): + self.ensure_one() + new_move = self.env['account.move'].sudo().with_company(self.company_id).with_context(default_move_type=move_vals['move_type']).create(move_vals) + message = _("This invoice has been created from the point of sale session: %s") % (self.id, self.name) + new_move.message_post(body=message) + if self.config_id.cash_rounding: + rounding_applied = float_round(self.amount_paid - self.amount_total, + precision_rounding=new_move.currency_id.rounding) + rounding_line = new_move.line_ids.filtered(lambda line: line.is_rounding_line) + if rounding_applied: + if rounding_applied > 0.0: + account_id = new_move.invoice_cash_rounding_id.loss_account_id.id + else: + account_id = new_move.invoice_cash_rounding_id.profit_account_id.id + if rounding_line: + if rounding_line.debit > 0: + rounding_line_difference = rounding_line.debit + rounding_applied + else: + rounding_line_difference = -rounding_line.credit + rounding_applied + if rounding_line_difference: + rounding_line.with_context(check_move_validity=False).write({ + 'debit': rounding_applied < 0.0 and -rounding_applied or 0.0, + 'credit': rounding_applied > 0.0 and rounding_applied or 0.0, + 'account_id': account_id, + 'price_unit': rounding_applied, + }) + existing_terms_line = new_move.line_ids.filtered( + lambda line: line.account_id.user_type_id.type in ('receivable', 'payable')) + if existing_terms_line.debit > 0: + existing_terms_line_new_val = float_round( + existing_terms_line.debit + rounding_line_difference, + precision_rounding=new_move.currency_id.rounding) + else: + existing_terms_line_new_val = float_round( + -existing_terms_line.credit + rounding_line_difference, + precision_rounding=new_move.currency_id.rounding) + existing_terms_line.write({ + 'debit': existing_terms_line_new_val > 0.0 and existing_terms_line_new_val or 0.0, + 'credit': existing_terms_line_new_val < 0.0 and -existing_terms_line_new_val or 0.0, + }) + + new_move._recompute_payment_terms_lines() + + else: + self.env['account.move.line'].create({ + 'debit': rounding_applied < 0.0 and -rounding_applied or 0.0, + 'credit': rounding_applied > 0.0 and rounding_applied or 0.0, + 'quantity': 1.0, + 'amount_currency': rounding_applied, + 'partner_id': new_move.partner_id.id, + 'move_id': new_move.id, + 'currency_id': new_move.currency_id if new_move.currency_id != new_move.company_id.currency_id else False, + 'company_id': new_move.company_id.id, + 'company_currency_id': new_move.company_id.currency_id.id, + 'is_rounding_line': True, + 'sequence': 9999, + 'name': new_move.invoice_cash_rounding_id.name, + 'account_id': account_id, + }) + else: + if rounding_line: + rounding_line.unlink() + return new_move + def action_pos_order_paid(self): self.ensure_one() @@ -381,8 +445,20 @@ class PosOrder(models.Model): else: total = float_round(self.amount_total, precision_rounding=self.config_id.rounding_method.rounding, rounding_method=self.config_id.rounding_method.rounding_method) - if not float_is_zero(total - self.amount_paid, precision_rounding=self.currency_id.rounding): + isPaid = float_is_zero(total - self.amount_paid, precision_rounding=self.currency_id.rounding) + + if not isPaid and not self.config_id.cash_rounding: raise UserError(_("Order %s is not fully paid.", self.name)) + elif not isPaid and self.config_id.cash_rounding: + currency = self.currency_id + if self.config_id.rounding_method.rounding_method == "HALF-UP": + maxDiff = currency.round(self.config_id.rounding_method.rounding / 2) + else: + maxDiff = currency.round(self.config_id.rounding_method.rounding) + + diff = currency.round(self.amount_total - self.amount_paid) + if not abs(diff) < maxDiff: + raise UserError(_("Order %s is not fully paid.", self.name)) self.write({'state': 'paid'}) @@ -424,12 +500,8 @@ class PosOrder(models.Model): raise UserError(_('Please provide a partner for the sale.')) move_vals = order._prepare_invoice_vals() - new_move = moves.sudo()\ - .with_company(order.company_id)\ - .with_context(default_move_type=move_vals['move_type'])\ - .create(move_vals) - message = _("This invoice has been created from the POS Order: %s") % (order.id, order.name) - new_move.message_post(body=message) + new_move = order._create_invoice(move_vals) + order.write({'account_move': new_move.id, 'state': 'invoiced'}) new_move.sudo().with_company(order.company_id)._post() moves += new_move diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js index e5dca209f9a..ab7e74092be 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js @@ -145,6 +145,15 @@ odoo.define('point_of_sale.PaymentScreen', function (require) { this.render(); } async validateOrder(isForceValidate) { + if(this.env.pos.config.cash_rounding) { + if(!this.env.pos.get_order().check_paymentlines_rounding()) { + this.showPopup('ErrorPopup', { + title: this.env._t('Rounding error in payment lines'), + body: this.env._t("The amount of your payment lines must be rounded to validate the transaction."), + }); + return; + } + } if (await this._isOrderValid(isForceValidate)) { // remove pending payments before finalizing the validation for (let line of this.paymentLines) { diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index bed03da34e2..ec9910d946c 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -3062,6 +3062,10 @@ exports.Order = Backbone.Model.extend({ newPaymentline.set_amount(this.get_due()); this.paymentlines.add(newPaymentline); this.select_paymentline(newPaymentline); + if(this.pos.config.cash_rounding){ + this.selected_paymentline.set_amount(0); + this.selected_paymentline.set_amount(this.get_due()); + } return newPaymentline; }, get_paymentlines: function(){ @@ -3298,12 +3302,13 @@ exports.Order = Backbone.Model.extend({ get_rounding_applied: function() { if(this.pos.config.cash_rounding) { const only_cash = this.pos.config.only_round_cash_method; - const has_cash = _.some(this.get_paymentlines(), function(pl) { return pl.payment_method.is_cash_count == true;}); + const has_cash = this.selected_paymentline ? this.selected_paymentline.payment_method.is_cash_count == true: false; if (!only_cash || (only_cash && has_cash)) { - var total = round_pr(this.get_total_with_tax(), this.pos.cash_rounding[0].rounding); + var remaining = this.get_total_with_tax() - this.get_total_paid(); + var total = round_pr(remaining, this.pos.cash_rounding[0].rounding); var sign = total > 0 ? 1.0 : -1.0; - var rounding_applied = total - (this.pos.config['iface_tax_included'] === "total"? this.get_subtotal(): this.get_total_with_tax()); + 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.decimals)){ @@ -3341,13 +3346,30 @@ exports.Order = Backbone.Model.extend({ return false; }, is_paid: function(){ - return this.get_due() <= 0; + return this.get_due() <= 0 && this.check_paymentlines_rounding(); }, is_paid_with_cash: function(){ return !!this.paymentlines.find( function(pl){ return pl.payment_method.is_cash_count; }); }, + check_paymentlines_rounding: function() { + if(this.pos.config.cash_rounding) { + var cash_rounding = this.pos.cash_rounding[0].rounding; + var default_rounding = this.pos.currency.rounding; + for(var id in this.get_paymentlines()) { + var line = this.get_paymentlines()[id]; + var diff = round_pr(round_pr(line.amount, cash_rounding) - round_pr(line.amount, default_rounding), default_rounding); + if(diff && line.payment_method.is_cash_count) { + return false; + } else if(!this.pos.config.only_round_cash_method && diff) { + return false; + } + } + return true; + } + return true; + }, finalize: function(){ this.destroy(); }, diff --git a/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml b/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml index 62767a3c7a1..9950009ebff 100644 --- a/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml +++ b/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml @@ -127,9 +127,13 @@
- ROUNDED - + Rounding +
+
+ To Pay + +