diff --git a/pos_partial_payment_odoo/__manifest__.py b/pos_partial_payment_odoo/__manifest__.py index 11bd08dca..f3115ff78 100644 --- a/pos_partial_payment_odoo/__manifest__.py +++ b/pos_partial_payment_odoo/__manifest__.py @@ -21,7 +21,7 @@ ################################################################################ { 'name': 'POS Partial Payment', - 'version': '16.0.1.0.0', + 'version': '16.0.1.0.1', 'category': 'Point of Sale', 'summary': "Simplify Payments with Partial Payment in Odoo POS", 'description': "In Odoo POS, partial payments allow customers to pay for " diff --git a/pos_partial_payment_odoo/doc/RELEASE_NOTES.md b/pos_partial_payment_odoo/doc/RELEASE_NOTES.md index eaec5732f..07ebe708f 100644 --- a/pos_partial_payment_odoo/doc/RELEASE_NOTES.md +++ b/pos_partial_payment_odoo/doc/RELEASE_NOTES.md @@ -5,3 +5,8 @@ ### ADD - Initial Commit for POS Partial Payment + +#### 03.11.2025 +#### Version 16.0.1.0.1 +#### UPDT +- Bug fix in amount_due miscalculation due to premature rounding in Pos invoice. diff --git a/pos_partial_payment_odoo/models/pos_order.py b/pos_partial_payment_odoo/models/pos_order.py index f670ec0af..6628d6b4f 100644 --- a/pos_partial_payment_odoo/models/pos_order.py +++ b/pos_partial_payment_odoo/models/pos_order.py @@ -21,8 +21,14 @@ ################################################################################ from odoo import api, models, fields, _ from odoo.exceptions import UserError -from odoo.osv.expression import AND -from odoo.tools import float_round, float_is_zero +from odoo.osv.expression import AND, _logger +from odoo.tools.float_utils import float_round, float_is_zero, float_compare + +try: + # Optional: use psycopg2 constants if available for more robust checks + from psycopg2 import errors as pg_errors +except Exception: + pg_errors = None class PosOrder(models.Model): @@ -77,25 +83,35 @@ class PosOrder(models.Model): payment. """ self.ensure_one() - # TODO: add support for mix of cash and non-cash payments when both cash_rounding and only_round_cash_method are True - if not self.config_id.cash_rounding \ - or self.config_id.only_round_cash_method \ - and not any( - p.payment_method_id.is_cash_count for p in self.payment_ids): + # compute the total to compare taking into account cash rounding config on the order's config + if (not self.config_id.cash_rounding) or ( + self.config_id.only_round_cash_method and not any( + p.payment_method_id.is_cash_count for p in self.payment_ids)): total = self.amount_total else: + # use the configured rounding method value and rounding algorithm if present + precision_rounding = getattr(self.config_id.rounding_method, + 'rounding', + self.currency_id.rounding) or self.currency_id.rounding total = float_round(self.amount_total, - precision_rounding=self.config_id.rounding_method.rounding, - rounding_method=self.config_id.rounding_method.rounding_method) + precision_rounding=precision_rounding, + rounding_method=getattr( + self.config_id.rounding_method, + 'rounding_method', None)) + isPaid = float_is_zero(total - self.amount_paid, precision_rounding=self.currency_id.rounding) + + # Only allow marking as paid for partial payments when this order specifically is flagged as partial payment if not isPaid: - pos_config = self.env['pos.config'].search([]) - for shop in pos_config: - if shop.partial_payment: - isPaid = True + # previously code flipped isPaid True if any pos_config.partial_payment existed, + # which caused incorrect marking. Instead: only allow if the *current order* is a partial payment order. + if self.is_partial_payment: + isPaid = True + + # Keep the cash rounding tolerance check if still not isPaid (original behavior) if not isPaid and not self.config_id.cash_rounding: - raise UserError(_("Order %s is not fully paid.", self.name)) + 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": @@ -104,10 +120,10 @@ class PosOrder(models.Model): 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)) + raise UserError(_("Order %s is not fully paid.") % (self.name,)) + self.write({'state': 'paid'}) return True @@ -123,3 +139,124 @@ class PosOrder(models.Model): offset=offset).ids totalCount = self.search_count(real_domain) return {'ids': ids, 'totalCount': totalCount} + + def _create_invoice(self, move_vals): + """ + Create the account.move (invoice) and apply cash rounding adjustments + ONLY if the order is effectively paid within rounding tolerance. + + NOTE: do NOT call super()._create_invoice(move_vals) because the original + implementation already contains rounding logic that would run unconditionally. + """ + self.ensure_one() + + # Create the invoice move directly (same as original POS code) + new_move = self.env['account.move'].sudo().with_company( + self.company_id).with_context( + default_move_type=move_vals['move_type'] + ).create(move_vals) + + # Post a message on the invoice linking to the POS order + message = _( + "This invoice has been created from the point of sale session: %s", + self._get_html_link(), + ) + new_move.message_post(body=message) + + # If no cash rounding configured for the POS, nothing to do + if not self.config_id.cash_rounding: + return new_move + + # determine rounding precision + rounding_precision = new_move.currency_id.rounding or self.currency_id.rounding or 0.0 + + # Determine whether the order is paid within rounding tolerance + is_paid_within_rounding = float_compare(self.amount_paid, + self.amount_total, + precision_rounding=rounding_precision) >= 0 + + # If only_round_cash_method is set, require at least one cash payment + if self.config_id.only_round_cash_method and not any( + p.payment_method_id.is_cash_count for p in self.payment_ids): + apply_rounding = False + else: + apply_rounding = is_paid_within_rounding + + # If not applying rounding, return the invoice as-is + if not apply_rounding: + return new_move + + # Compute rounding applied and adjust rounding + receivable lines + rounding_applied = float_round(self.amount_paid - self.amount_total, + precision_rounding=rounding_precision) + + rounding_line_difference = 0.0 + rounding_line = new_move.line_ids.filtered( + lambda line: line.display_type == 'rounding') + + if rounding_line and rounding_line.filtered(lambda l: l.debit > 0): + rl = rounding_line.filtered(lambda l: l.debit > 0)[0] + rounding_line_difference = rl.debit + rounding_applied + elif rounding_line and rounding_line.filtered(lambda l: l.credit > 0): + rl = rounding_line.filtered(lambda l: l.credit > 0)[0] + rounding_line_difference = -rl.credit + rounding_applied + else: + rounding_line_difference = rounding_applied + + 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_difference: + rounding_line.with_context(skip_invoice_sync=True, + 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, + }) + else: + # create the rounding line + self.env['account.move.line'].with_context( + skip_invoice_sync=True, check_move_validity=False).create({ + 'balance': -rounding_applied, + 'quantity': 1.0, + 'partner_id': new_move.partner_id.id, + 'move_id': new_move.id, + 'currency_id': new_move.currency_id.id, + 'company_id': new_move.company_id.id, + 'company_currency_id': new_move.company_id.currency_id.id, + 'display_type': 'rounding', + 'sequence': 9999, + 'name': self.config_id.rounding_method.name, + 'account_id': account_id, + }) + else: + if rounding_line: + rounding_line.with_context(skip_invoice_sync=True, + check_move_validity=False).unlink() + + if rounding_line_difference: + existing_terms_line = new_move.line_ids.filtered( + lambda line: line.account_id.account_type in ( + 'asset_receivable', 'liability_payable')) + if existing_terms_line: + if existing_terms_line.debit > 0: + existing_terms_line_new_val = float_round( + existing_terms_line.debit + rounding_line_difference, + precision_rounding=rounding_precision) + else: + existing_terms_line_new_val = float_round( + -existing_terms_line.credit + rounding_line_difference, + precision_rounding=rounding_precision) + existing_terms_line.with_context(skip_invoice_sync=True).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, + }) + + return new_move +