From 76dc355c6c5391b26c138079576387756fe45dac Mon Sep 17 00:00:00 2001 From: luis Date: Mon, 21 Sep 2026 16:43:45 +0200 Subject: [PATCH] #4 fix payment registration --- .../models/account_move.py | 18 ++++++-- .../tests/test_membership_payment_date.py | 46 +++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/membership_payment_date/models/account_move.py b/membership_payment_date/models/account_move.py index 6c833b0..5eeb797 100644 --- a/membership_payment_date/models/account_move.py +++ b/membership_payment_date/models/account_move.py @@ -53,14 +53,24 @@ class AccountMove(models.Model): return res def _get_membership_payment_date(self, invoice): - """Get the date of the most recent reconciled payment. + """Get the date of the most recent payment reconciled with the invoice. - If there are no payments (e.g. zero-amount invoice), fallback to today. + Uses the ``_get_reconciled_payments()`` helper instead of the + ``reconciled_payment_ids`` field: that field was dropped in 18.0 and + only reintroduced on 2025-10-13 (odoo/odoo d8e7798d2c), so it is + missing on older 18.0 builds. ``matched_payment_ids`` is not an option + either: the register wizard links it *after* reconciling, so it is + still empty when ``_invoice_paid_hook`` runs. + + If nothing is reconciled (e.g. zero-amount invoice), fallback to today. """ - self.ensure_one() - payments = invoice.reconciled_payment_ids + payments = invoice._get_reconciled_payments() if payments: return max(payments.mapped("date")) + # Counterparts that are not payments: credit notes, manual entries... + reconciled_amls = invoice._get_reconciled_amls() + if reconciled_amls: + return max(reconciled_amls.mapped("date")) return fields.Date.today() def _compute_membership_dates( diff --git a/membership_payment_date/tests/test_membership_payment_date.py b/membership_payment_date/tests/test_membership_payment_date.py index fdca809..fb6f3ea 100644 --- a/membership_payment_date/tests/test_membership_payment_date.py +++ b/membership_payment_date/tests/test_membership_payment_date.py @@ -110,6 +110,15 @@ class TestMembershipPaymentDate(common.TransactionCase): } ) cls.product_variable = template_variable.product_variant_id + # Plain service, not a membership + template_service = cls.env["product.template"].create( + { + "name": "Consulting Service", + "type": "service", + "list_price": 50.0, + } + ) + cls.product_service = template_service.product_variant_id def _create_invoice(self, product, invoice_date, quantity=1.0): """Create and return a draft out_invoice with the given product.""" @@ -311,3 +320,40 @@ class TestMembershipPaymentDate(common.TransactionCase): self._register_payment(invoice, payment_date=date(2026, 5, 10)) self.assertEqual(mline.date_from, date(2026, 5, 10)) self.assertEqual(mline.date_to, date(2026, 12, 31)) + + def test_09_single_payment_for_several_invoices(self): + """One payment settling several invoices at once. + + The reconciliation calls _invoice_paid_hook with a multi-record + recordset, which used to raise "Expected singleton". + """ + membership_invoice = self._create_invoice( + self.product_fixed, invoice_date=date(2026, 5, 1) + ) + mline = self._post_invoice(membership_invoice)[0] + other_invoice = self._create_invoice( + self.product_service, invoice_date=date(2026, 5, 1) + ) + other_invoice.action_post() + invoices = membership_invoice | other_invoice + payment_register = ( + self.env["account.payment.register"] + .with_context( + active_model="account.move", + active_ids=invoices.ids, + ) + .create( + { + "payment_date": date(2026, 5, 10), + "journal_id": self.bank_journal.id, + "group_payment": True, + } + ) + ) + payment_register._create_payments() + self.assertTrue( + all(inv.payment_state in ("paid", "in_payment") for inv in invoices) + ) + # The membership line still gets the payment date, not today's date + self.assertEqual(mline.date_from, date(2026, 5, 10)) + self.assertEqual(mline.date_to, date(2026, 12, 31))