[ADD] account_move_last_payment display last date and payment method on invoice
This commit is contained in:
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)
|
||||
Reference in New Issue
Block a user