[ADD] account_move_last_payment display last date and payment method on invoice
This commit is contained in:
33
account_move_last_payment/README.md
Normal file
33
account_move_last_payment/README.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Account Move Last Payment
|
||||||
|
|
||||||
|
Expose on `account.move` the date and method of the most recent real payment reconciled
|
||||||
|
with an invoice, as two stored, exportable fields shown in the invoice list view.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- `last_payment_date` (Date): date of the most recent payment reconciled with the
|
||||||
|
invoice — partial payments included.
|
||||||
|
- `last_payment_method` (Char): method of that most recent payment: the payment method
|
||||||
|
line name for explicit payments, "Bank Transfer" when the invoice was settled through
|
||||||
|
a direct bank statement line reconciliation.
|
||||||
|
- Covers both Community reconciliation paths: explicit payment ("Register Payment"
|
||||||
|
wizard) and direct bank statement reconciliation. Manual reconciliations without
|
||||||
|
payment or statement line are ignored, as are exchange difference moves.
|
||||||
|
- Recomputes automatically on reconcile / unreconcile and when a payment or statement
|
||||||
|
line date changes.
|
||||||
|
- Both fields are stored and displayed as optional columns of the invoice list view, so
|
||||||
|
they are exportable.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Depends on the `account` module only.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
### Contributors
|
||||||
|
|
||||||
|
- Elabore - Assisted by AI
|
||||||
|
|
||||||
|
### Maintainer
|
||||||
|
|
||||||
|
- Elabore
|
||||||
1
account_move_last_payment/__init__.py
Normal file
1
account_move_last_payment/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
21
account_move_last_payment/__manifest__.py
Normal file
21
account_move_last_payment/__manifest__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Copyright 2026 Elabore
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Account Move Last Payment",
|
||||||
|
"version": "18.0.1.0.0",
|
||||||
|
"summary": "Last real payment date and method on invoices",
|
||||||
|
"author": "Elabore",
|
||||||
|
"website": "https://git.elabore.coop/elabore/account-tools",
|
||||||
|
"license": "AGPL-3",
|
||||||
|
"category": "Accounting",
|
||||||
|
"depends": [
|
||||||
|
"account",
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
"views/account_move_views.xml",
|
||||||
|
],
|
||||||
|
"installable": True,
|
||||||
|
"auto_install": False,
|
||||||
|
"application": False,
|
||||||
|
}
|
||||||
54
account_move_last_payment/i18n/fr.po
Normal file
54
account_move_last_payment/i18n/fr.po
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Translation of Odoo Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * account_move_last_payment
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Odoo Server 18.0\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2026-10-20 15:23+0000\n"
|
||||||
|
"PO-Revision-Date: 2026-10-20 15:23+0000\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Plural-Forms: \n"
|
||||||
|
|
||||||
|
#. module: account_move_last_payment
|
||||||
|
#: code:addons/account_move_last_payment/models/account_move.py:0
|
||||||
|
msgid "Bank Transfer"
|
||||||
|
msgstr "Virement"
|
||||||
|
|
||||||
|
#. module: account_move_last_payment
|
||||||
|
#: model:ir.model.fields,field_description:account_move_last_payment.field_account_move__last_payment_date
|
||||||
|
msgid "Last Payment Date"
|
||||||
|
msgstr "Date du dernier paiement"
|
||||||
|
|
||||||
|
#. module: account_move_last_payment
|
||||||
|
#: model:ir.model.fields,field_description:account_move_last_payment.field_account_move__last_payment_method
|
||||||
|
msgid "Last Payment Method"
|
||||||
|
msgstr "Mode de paiement"
|
||||||
|
|
||||||
|
#. module: account_move_last_payment
|
||||||
|
#: model:ir.model.fields,help:account_move_last_payment.field_account_move__last_payment_date
|
||||||
|
msgid ""
|
||||||
|
"Date of the most recent payment reconciled with this invoice, whatever "
|
||||||
|
"the reconciliation path (explicit payment or direct bank statement line "
|
||||||
|
"reconciliation)."
|
||||||
|
msgstr ""
|
||||||
|
"Date du paiement le plus récent lettré avec cette facture, quel que soit "
|
||||||
|
"le chemin de lettrage (paiement explicite ou rapprochement direct d'une "
|
||||||
|
"ligne de relevé bancaire)."
|
||||||
|
|
||||||
|
#. module: account_move_last_payment
|
||||||
|
#: model:ir.model.fields,help:account_move_last_payment.field_account_move__last_payment_method
|
||||||
|
msgid ""
|
||||||
|
"Payment method of the most recent payment reconciled with this invoice."
|
||||||
|
msgstr ""
|
||||||
|
"Mode de paiement du paiement le plus récent lettré avec cette facture."
|
||||||
|
|
||||||
|
#. module: account_move_last_payment
|
||||||
|
#: model:ir.model,name:account_move_last_payment.model_account_move
|
||||||
|
msgid "Journal Entry"
|
||||||
|
msgstr "Écriture comptable"
|
||||||
1
account_move_last_payment/models/__init__.py
Normal file
1
account_move_last_payment/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import account_move
|
||||||
84
account_move_last_payment/models/account_move.py
Normal file
84
account_move_last_payment/models/account_move.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class AccountMove(models.Model):
|
||||||
|
_inherit = "account.move"
|
||||||
|
|
||||||
|
last_payment_date = fields.Date(
|
||||||
|
compute="_compute_last_payment_info",
|
||||||
|
store=True,
|
||||||
|
copy=False,
|
||||||
|
help="Date of the most recent payment reconciled with this "
|
||||||
|
"invoice, whatever the reconciliation path (explicit payment "
|
||||||
|
"or direct bank statement line reconciliation).",
|
||||||
|
)
|
||||||
|
last_payment_method = fields.Char(
|
||||||
|
compute="_compute_last_payment_info",
|
||||||
|
store=True,
|
||||||
|
copy=False,
|
||||||
|
help="Payment method of the most recent payment reconciled with this invoice.",
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends(
|
||||||
|
"line_ids.matched_debit_ids.debit_move_id.date",
|
||||||
|
"line_ids.matched_debit_ids.debit_move_id.move_id.origin_payment_id.payment_method_line_id.name",
|
||||||
|
"line_ids.matched_debit_ids.debit_move_id.move_id.statement_line_id",
|
||||||
|
"line_ids.matched_credit_ids.credit_move_id.date",
|
||||||
|
"line_ids.matched_credit_ids.credit_move_id.move_id.origin_payment_id.payment_method_line_id.name",
|
||||||
|
"line_ids.matched_credit_ids.credit_move_id.move_id.statement_line_id",
|
||||||
|
)
|
||||||
|
def _compute_last_payment_info(self):
|
||||||
|
"""Compute the last real payment date and method.
|
||||||
|
|
||||||
|
Only customer and vendor invoices carry the information; every
|
||||||
|
other move type gets both fields cleared. Candidates are the
|
||||||
|
reconciliation counterpart lines of the receivable/payable
|
||||||
|
lines (exchange difference moves excluded). The most recent
|
||||||
|
candidate wins; on equal dates the first candidate found wins.
|
||||||
|
"""
|
||||||
|
for move in self:
|
||||||
|
if move.move_type not in ("in_invoice", "out_invoice"):
|
||||||
|
move.last_payment_date = False
|
||||||
|
move.last_payment_method = False
|
||||||
|
continue
|
||||||
|
candidates = move._get_last_payment_candidates()
|
||||||
|
if not candidates:
|
||||||
|
move.last_payment_date = False
|
||||||
|
move.last_payment_method = False
|
||||||
|
else:
|
||||||
|
latest_date, latest_method = max(
|
||||||
|
candidates, key=lambda candidate: candidate[0]
|
||||||
|
)
|
||||||
|
move.last_payment_date = latest_date
|
||||||
|
move.last_payment_method = latest_method
|
||||||
|
|
||||||
|
def _get_last_payment_candidates(self):
|
||||||
|
"""Collect the candidate real payments for this invoice.
|
||||||
|
|
||||||
|
A candidate is a ``(date, method)`` tuple built from each
|
||||||
|
reconciliation counterpart line:
|
||||||
|
|
||||||
|
- counterpart move with ``origin_payment_id``: explicit
|
||||||
|
payment, dated with the payment date, method from the
|
||||||
|
payment method line;
|
||||||
|
- counterpart move with ``statement_line_id``: direct bank
|
||||||
|
statement reconciliation, dated with the counterpart move
|
||||||
|
date, method fixed to the bank transfer label;
|
||||||
|
- any other manual reconciliation is ignored.
|
||||||
|
|
||||||
|
:return: list of ``(date, method)`` tuples, possibly empty.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
invoice_partials, exchange_diff_moves = self._get_reconciled_invoices_partials()
|
||||||
|
candidates = []
|
||||||
|
for _partial, _amount, counterpart_line in invoice_partials:
|
||||||
|
if counterpart_line.move_id.id in exchange_diff_moves:
|
||||||
|
continue
|
||||||
|
payment = counterpart_line.move_id.origin_payment_id
|
||||||
|
if payment:
|
||||||
|
candidates.append((payment.date, payment.payment_method_line_id.name))
|
||||||
|
elif counterpart_line.move_id.statement_line_id:
|
||||||
|
candidates.append(
|
||||||
|
(counterpart_line.move_id.date, self.env._("Bank Transfer"))
|
||||||
|
)
|
||||||
|
return candidates
|
||||||
1
account_move_last_payment/tests/__init__.py
Normal file
1
account_move_last_payment/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import test_last_payment
|
||||||
239
account_move_last_payment/tests/test_last_payment.py
Normal file
239
account_move_last_payment/tests/test_last_payment.py
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
from odoo import fields
|
||||||
|
from odoo.tests import tagged
|
||||||
|
|
||||||
|
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||||
|
|
||||||
|
|
||||||
|
@tagged("post_install", "-at_install")
|
||||||
|
class TestAccountMoveLastPayment(AccountTestInvoicingCommon):
|
||||||
|
"""Test the last payment date and method stored on invoices."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
|
||||||
|
cls.inbound_method_line = cls.company_data[
|
||||||
|
"default_journal_bank"
|
||||||
|
].inbound_payment_method_line_ids[0]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_posted_invoice(cls, move_type="out_invoice"):
|
||||||
|
"""Create and post a simple invoice without taxes.
|
||||||
|
|
||||||
|
:param str move_type: the invoice type to create.
|
||||||
|
:return: the posted account.move record.
|
||||||
|
"""
|
||||||
|
return cls.init_invoice(move_type, post=True, amounts=[1000.0], taxes=[])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _register_payment(cls, invoice, date=None, method_line=None, amount=None):
|
||||||
|
"""Register a payment on an invoice through the payment wizard.
|
||||||
|
|
||||||
|
:param invoice: the account.move to pay.
|
||||||
|
:param date: the payment date (defaults to today).
|
||||||
|
:param method_line: the account.payment.method.line to use
|
||||||
|
(defaults to the bank journal inbound method line).
|
||||||
|
:param amount: the payment amount (defaults to the invoice
|
||||||
|
residual).
|
||||||
|
:return: the created account.payment record.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
cls.env["account.payment.register"]
|
||||||
|
.with_context(
|
||||||
|
active_model="account.move",
|
||||||
|
active_ids=invoice.ids,
|
||||||
|
)
|
||||||
|
.create(
|
||||||
|
{
|
||||||
|
"payment_date": date or fields.Date.today(),
|
||||||
|
"payment_method_line_id": (
|
||||||
|
method_line or cls.inbound_method_line
|
||||||
|
).id,
|
||||||
|
"amount": amount if amount is not None else invoice.amount_residual,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
._create_payments()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _reconcile_statement_line(self, invoice, amount, date=None):
|
||||||
|
"""Create a bank statement line and reconcile it with the invoice.
|
||||||
|
|
||||||
|
Mirrors the core reconciliation path: the statement line suspense
|
||||||
|
counterpart is re-accounted to the invoice receivable account,
|
||||||
|
then both lines are reconciled together.
|
||||||
|
|
||||||
|
:param invoice: the account.move to settle.
|
||||||
|
:param float amount: the statement line amount.
|
||||||
|
:param date: the statement line date (defaults to today).
|
||||||
|
:return: the created account.bank.statement.line record.
|
||||||
|
"""
|
||||||
|
statement_line = self.env["account.bank.statement.line"].create(
|
||||||
|
{
|
||||||
|
"payment_ref": invoice.name,
|
||||||
|
"journal_id": self.company_data["default_journal_bank"].id,
|
||||||
|
"partner_id": invoice.partner_id.id,
|
||||||
|
"amount": amount,
|
||||||
|
"date": date or fields.Date.today(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
st_liquidity_lines, st_suspense_lines, _ = statement_line._seek_for_lines()
|
||||||
|
receivable_lines = invoice.line_ids.filtered(
|
||||||
|
lambda line: line.account_type == "asset_receivable"
|
||||||
|
)
|
||||||
|
st_suspense_lines.account_id = receivable_lines.account_id
|
||||||
|
(st_suspense_lines + receivable_lines).reconcile()
|
||||||
|
return statement_line
|
||||||
|
|
||||||
|
def test_register_payment(self):
|
||||||
|
"""Explicit payment: fields match the payment date and method."""
|
||||||
|
invoice = self._create_posted_invoice()
|
||||||
|
payment = self._register_payment(invoice)
|
||||||
|
self.assertEqual(invoice.last_payment_date, payment.date)
|
||||||
|
self.assertEqual(
|
||||||
|
invoice.last_payment_method, payment.payment_method_line_id.name
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_bank_statement_reconciliation(self):
|
||||||
|
"""Direct statement reconciliation: statement date + Bank Transfer."""
|
||||||
|
invoice = self._create_posted_invoice()
|
||||||
|
yesterday = fields.Date.subtract(fields.Date.today(), days=1)
|
||||||
|
statement_line = self._reconcile_statement_line(
|
||||||
|
invoice, invoice.amount_total, date=yesterday
|
||||||
|
)
|
||||||
|
self.assertEqual(invoice.last_payment_date, statement_line.date)
|
||||||
|
self.assertEqual(invoice.last_payment_method, "Bank Transfer")
|
||||||
|
|
||||||
|
def test_list_view_and_export(self):
|
||||||
|
"""Both fields are stored and shown in the invoice list view."""
|
||||||
|
self.assertTrue(self.env["account.move"]._fields["last_payment_date"].store)
|
||||||
|
self.assertTrue(self.env["account.move"]._fields["last_payment_method"].store)
|
||||||
|
view = self.env["ir.ui.view"]._get_view_id("account.view_invoice_tree")
|
||||||
|
arch = self.env["account.move"].get_view(view_id=view, view_type="list")["arch"]
|
||||||
|
self.assertIn('name="last_payment_date"', arch)
|
||||||
|
self.assertIn('name="last_payment_method"', arch)
|
||||||
|
|
||||||
|
def test_multiple_payments_keeps_latest(self):
|
||||||
|
"""With several payments, the most recent candidate wins."""
|
||||||
|
invoice = self._create_posted_invoice()
|
||||||
|
older_date = fields.Date.subtract(fields.Date.today(), days=10)
|
||||||
|
newer_date = fields.Date.subtract(fields.Date.today(), days=2)
|
||||||
|
self._register_payment(invoice, date=older_date, amount=400.0)
|
||||||
|
self._register_payment(invoice, date=newer_date, amount=600.0)
|
||||||
|
self.assertEqual(invoice.last_payment_date, newer_date)
|
||||||
|
self.assertEqual(invoice.last_payment_method, self.inbound_method_line.name)
|
||||||
|
|
||||||
|
def test_statement_line_date_change_recompute(self):
|
||||||
|
"""Changing the statement line date updates the stored fields."""
|
||||||
|
invoice = self._create_posted_invoice()
|
||||||
|
yesterday = fields.Date.subtract(fields.Date.today(), days=1)
|
||||||
|
statement_line = self._reconcile_statement_line(
|
||||||
|
invoice, invoice.amount_total, date=yesterday
|
||||||
|
)
|
||||||
|
self.assertEqual(invoice.last_payment_date, yesterday)
|
||||||
|
older_date = fields.Date.subtract(fields.Date.today(), days=30)
|
||||||
|
statement_line.date = older_date
|
||||||
|
self.assertEqual(invoice.last_payment_date, older_date)
|
||||||
|
|
||||||
|
def test_unreconcile_clears_fields(self):
|
||||||
|
"""Unreconciling the invoice clears both fields."""
|
||||||
|
invoice = self._create_posted_invoice()
|
||||||
|
self._register_payment(invoice)
|
||||||
|
invoice.line_ids.remove_move_reconcile()
|
||||||
|
self.assertFalse(invoice.last_payment_date)
|
||||||
|
self.assertFalse(invoice.last_payment_method)
|
||||||
|
|
||||||
|
def _setup_eur_rates(self, invoice_date, payment_date):
|
||||||
|
"""Set deterministic EUR rates so the payment creates an
|
||||||
|
exchange difference move.
|
||||||
|
|
||||||
|
:param invoice_date: the invoice posting date.
|
||||||
|
:param payment_date: the payment date.
|
||||||
|
:return: the EUR res.currency record.
|
||||||
|
"""
|
||||||
|
eur = self.env.ref("base.EUR")
|
||||||
|
if not eur.active:
|
||||||
|
eur.active = True
|
||||||
|
self.env["res.currency.rate"].search(
|
||||||
|
[
|
||||||
|
("currency_id", "=", eur.id),
|
||||||
|
("name", "in", [invoice_date, payment_date]),
|
||||||
|
]
|
||||||
|
).unlink()
|
||||||
|
self.env["res.currency.rate"].create(
|
||||||
|
[
|
||||||
|
{"currency_id": eur.id, "name": invoice_date, "rate": 1.0},
|
||||||
|
{"currency_id": eur.id, "name": payment_date, "rate": 1.1},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return eur
|
||||||
|
|
||||||
|
def test_multicurrency_invoice(self):
|
||||||
|
"""A foreign currency payment keeps its real date and method,
|
||||||
|
and the generated exchange difference move is not taken as a
|
||||||
|
payment candidate."""
|
||||||
|
payment_date = fields.Date.subtract(fields.Date.today(), days=5)
|
||||||
|
invoice_date = fields.Date.from_string("2019-01-01")
|
||||||
|
eur = self._setup_eur_rates(invoice_date, payment_date)
|
||||||
|
invoice = self.init_invoice("out_invoice", post=False, amounts=[1000.0])
|
||||||
|
invoice.currency_id = eur
|
||||||
|
invoice.action_post()
|
||||||
|
payment = self._register_payment(
|
||||||
|
invoice,
|
||||||
|
date=payment_date,
|
||||||
|
amount=invoice.amount_residual,
|
||||||
|
)
|
||||||
|
self.assertTrue(invoice.line_ids.mapped("matched_credit_ids.exchange_move_id"))
|
||||||
|
candidates = invoice._get_last_payment_candidates()
|
||||||
|
self.assertEqual(len(candidates), 1)
|
||||||
|
self.assertEqual(invoice.last_payment_date, payment.date)
|
||||||
|
self.assertEqual(
|
||||||
|
invoice.last_payment_method, payment.payment_method_line_id.name
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_draft_invoice_is_empty(self):
|
||||||
|
"""A draft invoice has never been reconciled, so both fields
|
||||||
|
stay empty."""
|
||||||
|
invoice = self.init_invoice("out_invoice", post=False, amounts=[1000.0])
|
||||||
|
self.assertFalse(invoice.last_payment_date)
|
||||||
|
self.assertFalse(invoice.last_payment_method)
|
||||||
|
|
||||||
|
def test_non_invoice_moves_are_empty(self):
|
||||||
|
"""Journal entries and credit notes never carry the fields."""
|
||||||
|
invoice = self._create_posted_invoice()
|
||||||
|
self._register_payment(invoice)
|
||||||
|
refund = self._create_posted_invoice(move_type="out_refund")
|
||||||
|
entry = self.env["account.move"].create(
|
||||||
|
{
|
||||||
|
"move_type": "entry",
|
||||||
|
"journal_id": self.company_data["default_journal_misc"].id,
|
||||||
|
"date": fields.Date.today(),
|
||||||
|
"line_ids": [
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"account_id": self.company_data[
|
||||||
|
"default_account_revenue"
|
||||||
|
].id,
|
||||||
|
"debit": 100.0,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"account_id": self.company_data[
|
||||||
|
"default_account_receivable"
|
||||||
|
].id,
|
||||||
|
"credit": 100.0,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
entry.action_post()
|
||||||
|
self.assertFalse(refund.last_payment_date)
|
||||||
|
self.assertFalse(refund.last_payment_method)
|
||||||
|
self.assertFalse(entry.last_payment_date)
|
||||||
|
self.assertFalse(entry.last_payment_method)
|
||||||
|
self.assertTrue(invoice.last_payment_date)
|
||||||
14
account_move_last_payment/views/account_move_views.xml
Normal file
14
account_move_last_payment/views/account_move_views.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_invoice_tree_last_payment" model="ir.ui.view">
|
||||||
|
<field name="name">account.invoice.list.last.payment</field>
|
||||||
|
<field name="model">account.move</field>
|
||||||
|
<field name="inherit_id" ref="account.view_invoice_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<field name="amount_residual_signed" position="after">
|
||||||
|
<field name="last_payment_date" optional="show"/>
|
||||||
|
<field name="last_payment_method" optional="show"/>
|
||||||
|
</field>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user