diff --git a/account_payment_order/models/account_payment_order.py b/account_payment_order/models/account_payment_order.py index 028125940602..3eee79766e13 100644 --- a/account_payment_order/models/account_payment_order.py +++ b/account_payment_order/models/account_payment_order.py @@ -8,6 +8,7 @@ from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_compare class AccountPaymentOrder(models.Model): @@ -420,15 +421,50 @@ def open2generated(self): return action def generated2uploaded(self): + """Post payments and reconcile against source journal items + + Partially reconcile payments that don't match their source journal items, + then reconcile the rest in one go. + """ self.payment_ids.action_post() # Perform the reconciliation of payments and source journal items for payment in self.payment_ids: - ( - payment.payment_line_ids.move_line_id - + payment.move_id.line_ids.filtered( - lambda x: x.account_id == payment.destination_account_id - ) - ).reconcile() + payment_move_line_id = payment.move_id.line_ids.filtered( + lambda x: x.account_id == payment.destination_account_id + ) + apr = self.env["account.partial.reconcile"] + excl_pay_lines = self.env["account.payment.line"] + for line in payment.payment_line_ids: + if not line.move_line_id: + continue + sign = -1 if payment.payment_order_id.payment_type == "outbound" else 1 + if ( + float_compare( + line.amount_currency, + (line.move_line_id.amount_residual_currency * sign), + precision_rounding=line.move_line_id.currency_id.rounding, + ) + != 0 + ): + if line.move_line_id.amount_residual_currency < 0: + debit_move_id = payment_move_line_id.id + credit_move_id = line.move_line_id.id + else: + debit_move_id = line.move_line_id.id + credit_move_id = payment_move_line_id.id + apr.create( + { + "debit_move_id": debit_move_id, + "credit_move_id": credit_move_id, + "amount": abs(line.amount_company_currency), + "debit_amount_currency": abs(line.amount_currency), + "credit_amount_currency": abs(line.amount_currency), + } + ) + excl_pay_lines |= line + pay_lines = payment.payment_line_ids - excl_pay_lines + if pay_lines: + (pay_lines.move_line_id + payment_move_line_id).reconcile() self.write( {"state": "uploaded", "date_uploaded": fields.Date.context_today(self)} ) diff --git a/account_payment_order/tests/test_payment_order_inbound.py b/account_payment_order/tests/test_payment_order_inbound.py index c7c2b031499f..74e38c18795a 100644 --- a/account_payment_order/tests/test_payment_order_inbound.py +++ b/account_payment_order/tests/test_payment_order_inbound.py @@ -113,6 +113,10 @@ def test_creation(self): self.assertEqual(payment_order.state, "uploaded") with self.assertRaises(UserError): payment_order.unlink() + matching_number = ( + payment_order.payment_ids.payment_line_ids.move_line_id.matching_number + ) + self.assertTrue(matching_number and matching_number != "P") payment_order.action_uploaded_cancel() self.assertEqual(payment_order.state, "cancel") diff --git a/account_payment_order/tests/test_payment_order_outbound.py b/account_payment_order/tests/test_payment_order_outbound.py index a709c28f2281..4ea40d18a896 100644 --- a/account_payment_order/tests/test_payment_order_outbound.py +++ b/account_payment_order/tests/test_payment_order_outbound.py @@ -316,6 +316,62 @@ def test_manual_line_and_manual_date(self): fields.Date.context_today(outbound_order), ) + def test_partial_reconciliation(self): + """ + Confirm both supplier invoices + Add invoices to payment order + Reduce payment amount of first invoice from 100 to 80 + Take payment order all the way to uploaded + Confirm 80 reconciled with first, not second invoice + + generated2uploaded() does partial reconciliation of non-matching + line amounts before running .reconcile() against the remaining + matching line amounts. + """ + # Open both invoices + self.invoice.action_post() + self.invoice_02.action_post() + + # Add to payment order using the wizard + self.env["account.invoice.payment.line.multi"].with_context( + active_model="account.move", + active_ids=self.invoice.ids + self.invoice_02.ids, + ).create({}).run() + + payment_order = self.env["account.payment.order"].search(self.domain) + self.assertEqual(len(payment_order), 1) + + payment_order.write({"journal_id": self.bank_journal.id}) + + self.assertEqual(len(payment_order.payment_line_ids), 2) + self.assertFalse(payment_order.payment_ids) + + # Reduce payment of first invoice from 100 to 80 + first_payment_line, second_payment_line = payment_order.payment_line_ids + first_payment_line.write({"amount_currency": 80.0}) + + # Open payment order + payment_order.draft2open() + + # Confirm single payment (grouped - two invoices one partner) + self.assertEqual(payment_order.payment_count, 1) + + # Generate and upload + payment_order.open2generated() + payment_order.generated2uploaded() + + self.assertEqual(payment_order.state, "uploaded") + with self.assertRaises(UserError): + payment_order.unlink() + + # Confirm payments were reconciled against correct invoices + self.assertEqual(first_payment_line.amount_currency, 80.0) + self.assertEqual( + first_payment_line.move_line_id.amount_residual_currency, -20.0 + ) + self.assertEqual(second_payment_line.amount_currency, 100.0) + self.assertEqual(second_payment_line.move_line_id.amount_residual_currency, 0.0) + def test_supplier_refund(self): """ Confirm the supplier invoice