Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions openupgrade_scripts/scripts/account/18.0.1.3/post-migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,69 @@ def _handle_outstanding_accounts(env):
)


def fix_payment_paid_state(env):
"""Correct payments wrongly left in 'in_process' by fill_account_payment.

``fill_account_payment`` (pre-migration) derives account.payment.state only
from the payment's own journal entry: ``am.state`` / ``am.payment_state``.
But a payment move is an 'entry', and account.move._compute_payment_state
only computes a real payment_state for invoices -- so a payment move always
has payment_state = 'not_paid' on <=17. The ``WHEN am.payment_state = 'paid'``
branch is therefore dead for payments, and *every* posted payment is mapped
to 'in_process', including payments that Odoo 18 would classify as 'paid'.

On v18 a payment is 'paid' (see account.payment._compute_state / action_post)
when its liquidity line is fully reconciled OR its liquidity account is not
reconcilable (money posted straight to a bank/cash account, account_type
'asset_cash', instead of to a reconcilable outstanding account). The latter
is invisible on setups that use reconcilable outstanding accounts -- where
'in_process' is in fact correct -- which is why the issue only reproduces on
databases that post payments directly to the bank/cash account.

This reproduces that classification in bulk SQL (no per-record ORM cost of
_seek_for_lines): liquidity accounts = the journal default account plus every
payment-method-line payment account on that journal, a complete cover of
_get_valid_liquidity_accounts() (outstanding_account_id is itself just
payment_method_line_id.payment_account_id on v18). Runs after
_handle_outstanding_accounts so those payment accounts are populated.
"""
openupgrade.logged_query(
env.cr,
"""
WITH liq AS (
SELECT ap.id AS payment_id,
SUM(aml.amount_residual) AS residual,
BOOL_OR(aa.reconcile) AS any_reconcile,
cur.rounding AS rounding
FROM account_payment ap
JOIN account_move am ON am.id = ap.move_id
JOIN res_company comp ON comp.id = am.company_id
JOIN res_currency cur ON cur.id = comp.currency_id
JOIN account_journal aj ON aj.id = ap.journal_id
JOIN account_move_line aml ON aml.move_id = am.id
JOIN account_account aa ON aa.id = aml.account_id
WHERE ap.state = 'in_process'
AND (
aml.account_id = aj.default_account_id
OR aml.account_id IN (
SELECT apml.payment_account_id
FROM account_payment_method_line apml
WHERE apml.journal_id = ap.journal_id
AND apml.payment_account_id IS NOT NULL
)
)
GROUP BY ap.id, cur.rounding
)
UPDATE account_payment ap
SET state = 'paid',
is_matched = TRUE
FROM liq
WHERE ap.id = liq.payment_id
AND (NOT liq.any_reconcile OR ABS(liq.residual) < liq.rounding / 2.0)
""",
)


@openupgrade.migrate()
def migrate(env, version):
handle_lock_dates(env)
Expand All @@ -219,6 +282,7 @@ def migrate(env, version):
)
convert_company_dependent(env)
fill_res_partner_property_x_payment_method_line_id(env)
fix_payment_paid_state(env)
openupgrade.load_data(env, "account", "18.0.1.3/noupdate_changes.xml")
openupgrade.delete_record_translations(
env.cr, "account", ["email_template_edi_invoice"]
Expand Down
160 changes: 160 additions & 0 deletions openupgrade_scripts/scripts/account/tests/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,163 @@
}
).action_send_and_print()
env.cr.commit()

# ---------------------------------------------------------------------------
# Payment state migration scenarios.
#
# fill_account_payment (pre-migration) maps every posted payment to
# 'in_process', because a payment's own move always has payment_state
# 'not_paid' on <=17. That is wrong for payments Odoo 18 classifies as 'paid'
# (liquidity account not reconcilable, or liquidity line fully reconciled).
# We build the four reconciliation shapes in two journal configurations so the
# post-migration fix can be asserted:
# - DIRECT: liquidity posts to a non-reconcilable bank/cash account
# (account_type 'asset_cash') -> must migrate to 'paid'.
# - OUTSTANDING: liquidity posts to a reconcilable outstanding account
# (account_type 'asset_current'), never bank-reconciled
# -> must stay 'in_process'.
# ---------------------------------------------------------------------------
company = env.ref("base.main_company")

recv = env["account.account"].search(
[("account_type", "=", "asset_receivable"), ("company_id", "=", company.id)],
limit=1,
)
income = env["account.account"].search(
[("account_type", "=", "income"), ("company_id", "=", company.id)], limit=1
)

bank_acc = env["account.account"].create(
{
"name": "OU test direct bank",
"code": "OUDIR",
"account_type": "asset_cash",
"reconcile": False,
"company_id": company.id,
}
)
outstanding_acc = env["account.account"].create(
{
"name": "OU test outstanding",
"code": "OUOUT",
"account_type": "asset_current",
"reconcile": True,
"company_id": company.id,
}
)
outstanding_bank_acc = env["account.account"].create(
{
"name": "OU test outstanding bank",
"code": "OUOBK",
"account_type": "asset_cash",
"reconcile": False,
"company_id": company.id,
}
)

direct_journal = env["account.journal"].create(
{
"name": "OU test direct bank",
"code": "OUDBK",
"type": "bank",
"company_id": company.id,
"default_account_id": bank_acc.id,
}
)
# liquidity posts straight to the (non-reconcilable) bank account
direct_journal.inbound_payment_method_line_ids.payment_account_id = bank_acc.id
direct_journal.outbound_payment_method_line_ids.payment_account_id = bank_acc.id

outstanding_journal = env["account.journal"].create(
{
"name": "OU test outstanding bank",
"code": "OUOBK",
"type": "bank",
"company_id": company.id,
"default_account_id": outstanding_bank_acc.id,
}
)
# liquidity posts to the reconcilable outstanding account
outstanding_journal.inbound_payment_method_line_ids.payment_account_id = (
outstanding_acc.id
)
outstanding_journal.outbound_payment_method_line_ids.payment_account_id = (
outstanding_acc.id
)

sale_journal = env["account.journal"].search(
[("type", "=", "sale"), ("company_id", "=", company.id)], limit=1
)
ou_partner = env["res.partner"].create(
{
"name": "OU test payment partner",
"property_account_receivable_id": recv.id,
}
)


def _ou_invoice(amount):
inv = env["account.move"].create(
{
"move_type": "out_invoice",
"partner_id": ou_partner.id,
"journal_id": sale_journal.id,
"invoice_line_ids": [
(
0,
0,
{
"name": "ou test",
"quantity": 1,
"price_unit": amount,
"account_id": income.id,
"tax_ids": [(6, 0, [])],
},
)
],
}
)
inv.action_post()
return inv


def _ou_pay(invoices, journal, amount):
wizard = (
env["account.payment.register"]
.with_context(
active_model="account.move",
active_ids=invoices.ids,
)
.create({"journal_id": journal.id})
)
if len(invoices) > 1:
wizard.group_payment = True
wizard.amount = amount
return wizard._create_payments()


def _ou_build(prefix, journal):
# 1) partial payment of one invoice
p1 = _ou_pay(_ou_invoice(1000), journal, 400)
# 2) one payment paying multiple invoices partially
p2 = _ou_pay(_ou_invoice(1000) | _ou_invoice(1000), journal, 800)
# 3) a payment paying an invoice fully
p3 = _ou_pay(_ou_invoice(1000), journal, 1000)
# 4) two payments paying one invoice fully
inv = _ou_invoice(1000)
p4a = _ou_pay(inv, journal, 600)
p4b = _ou_pay(inv, journal, 400)
env["ir.model.data"]._update_xmlids(
[
{"xml_id": "openupgrade_test_account.%s_s1" % prefix, "record": p1},
{"xml_id": "openupgrade_test_account.%s_s2" % prefix, "record": p2},
{"xml_id": "openupgrade_test_account.%s_s3" % prefix, "record": p3},
{"xml_id": "openupgrade_test_account.%s_s4a" % prefix, "record": p4a},
{"xml_id": "openupgrade_test_account.%s_s4b" % prefix, "record": p4b},
]
)


_ou_build("ou_direct", direct_journal)
_ou_build("ou_outstanding", outstanding_journal)
env.cr.commit()
29 changes: 29 additions & 0 deletions openupgrade_scripts/scripts/account/tests/test_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,32 @@ def test_sending_data(self):
moves_with_sending_data[0].sending_data["author_partner_id"],
self.env.user.partner_id.id,
)

def test_payment_state_direct_to_bank(self):
"""Payments posting straight to a non-reconcilable bank/cash account
(account_type 'asset_cash') must migrate to 'paid' for every
reconciliation shape: a partial payment of one invoice, a payment paying
several invoices partially, a payment paying an invoice fully, and two
payments paying one invoice fully.
"""
for name in ("s1", "s2", "s3", "s4a", "s4b"):
payment = self.env.ref("openupgrade_test_account.ou_direct_%s" % name)
self.assertEqual(
payment.state,
"paid",
"ou_direct_%s should have migrated to 'paid'" % name,
)

def test_payment_state_outstanding(self):
"""The same four reconciliation shapes, but posting to a reconcilable
outstanding account (account_type 'asset_current') that was never
bank-reconciled, must stay 'in_process' -- the fix must not over-promote
them.
"""
for name in ("s1", "s2", "s3", "s4a", "s4b"):
payment = self.env.ref("openupgrade_test_account.ou_outstanding_%s" % name)
self.assertEqual(
payment.state,
"in_process",
"ou_outstanding_%s should have stayed 'in_process'" % name,
)
Loading