diff --git a/openupgrade_scripts/scripts/account/18.0.1.3/post-migration.py b/openupgrade_scripts/scripts/account/18.0.1.3/post-migration.py index 7e0e6a125c57..eff692902d92 100644 --- a/openupgrade_scripts/scripts/account/18.0.1.3/post-migration.py +++ b/openupgrade_scripts/scripts/account/18.0.1.3/post-migration.py @@ -208,6 +208,107 @@ def _handle_outstanding_accounts(env): ) +def fix_payment_paid_state(env): + """Set 'paid' on the payments that fill_account_payment left as 'in_process'. + + It reads the payment's own move, whose payment_state is always 'not_paid' + below v18 as it is only computed for invoices, so every posted payment ends + up 'in_process'. Apply the two conditions of account.payment._compute_state. + """ + # liquidity line fully reconciled, or on a non reconcilable account + openupgrade.logged_query( + env.cr, + """ + WITH liquidity AS ( + SELECT ap.id AS payment_id, + SUM(aml.amount_residual) AS residual, + BOOL_OR(aa.reconcile) AS reconcilable, + cur.rounding AS rounding + FROM account_payment ap + JOIN account_move am ON am.id = ap.move_id + JOIN res_company rc ON rc.id = am.company_id + JOIN res_currency cur ON cur.id = rc.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 + LEFT JOIN account_payment_method_line apml + ON apml.id = ap.payment_method_line_id + WHERE ap.state = 'in_process' + AND ( + aml.account_id = aj.default_account_id + OR aml.account_id = ap.outstanding_account_id + OR aml.account_id = apml.payment_account_id + OR aml.account_id IN ( + SELECT payment_account_id + FROM account_payment_method_line + WHERE journal_id = ap.journal_id + AND payment_account_id IS NOT NULL + ) + ) + GROUP BY ap.id, cur.rounding + ) + UPDATE account_payment ap + SET state = 'paid' + FROM liquidity + WHERE ap.id = liquidity.payment_id + AND ( + NOT liquidity.reconcilable + OR ABS(liquidity.residual) < liquidity.rounding / 2 + ) + """, + ) + # all the reconciled invoices, or all the reconciled bills, are paid + openupgrade.logged_query( + env.cr, + """ + WITH reconciled AS ( + SELECT DISTINCT ap.id AS payment_id, inv.id AS invoice_id, + inv.move_type, inv.payment_state + FROM account_payment ap + JOIN account_move am ON am.id = ap.move_id + JOIN account_move_line aml ON aml.move_id = am.id + JOIN account_account aa ON aa.id = aml.account_id + JOIN account_partial_reconcile apr + ON apr.debit_move_id = aml.id OR apr.credit_move_id = aml.id + JOIN account_move_line counterpart + ON (apr.debit_move_id = counterpart.id + OR apr.credit_move_id = counterpart.id) + AND counterpart.id != aml.id + JOIN account_move inv ON inv.id = counterpart.move_id + WHERE ap.state = 'in_process' + AND aa.account_type IN ('asset_receivable', 'liability_payable') + AND inv.move_type IN ( + 'out_invoice', 'out_refund', 'out_receipt', + 'in_invoice', 'in_refund', 'in_receipt' + ) + ), counts AS ( + SELECT payment_id, + COUNT(*) FILTER (WHERE move_type IN ( + 'out_invoice', 'out_refund', 'out_receipt')) AS invoices, + COUNT(*) FILTER (WHERE move_type IN ( + 'out_invoice', 'out_refund', 'out_receipt') + AND payment_state = 'paid') AS paid_invoices, + COUNT(*) FILTER (WHERE move_type IN ( + 'in_invoice', 'in_refund', 'in_receipt')) AS bills, + COUNT(*) FILTER (WHERE move_type IN ( + 'in_invoice', 'in_refund', 'in_receipt') + AND payment_state = 'paid') AS paid_bills + FROM reconciled + GROUP BY payment_id + ) + UPDATE account_payment ap + SET state = 'paid' + FROM counts + WHERE ap.id = counts.payment_id + AND ap.state = 'in_process' + AND ( + (counts.invoices > 0 AND counts.invoices = counts.paid_invoices) + OR (counts.bills > 0 AND counts.bills = counts.paid_bills) + ) + """, + ) + + @openupgrade.migrate() def migrate(env, version): handle_lock_dates(env) @@ -219,6 +320,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"] diff --git a/openupgrade_scripts/scripts/account/tests/data.py b/openupgrade_scripts/scripts/account/tests/data.py index 9b088680aa9a..bb685fedc8db 100644 --- a/openupgrade_scripts/scripts/account/tests/data.py +++ b/openupgrade_scripts/scripts/account/tests/data.py @@ -17,3 +17,151 @@ } ).action_send_and_print() env.cr.commit() + +# Payments for the state fix in post-migration, in the four reconciliation +# shapes: once on a journal paying from a plain bank account, once on a journal +# paying from a reconcilable outstanding account. +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() diff --git a/openupgrade_scripts/scripts/account/tests/test_migration.py b/openupgrade_scripts/scripts/account/tests/test_migration.py index e9d14568d468..c2132b60ef11 100644 --- a/openupgrade_scripts/scripts/account/tests/test_migration.py +++ b/openupgrade_scripts/scripts/account/tests/test_migration.py @@ -23,3 +23,39 @@ 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 from a non reconcilable bank account are paid, whatever the + reconciliation shape. + """ + 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_not_fully_paid(self): + """Payments from an outstanding account stay in process while the + invoices they are reconciled with are not paid. + """ + for name in ("s1", "s2"): + 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, + ) + + def test_payment_state_outstanding_fully_paid(self): + """Payments from an outstanding account are paid once all the invoices + they are reconciled with are. + """ + for name in ("s3", "s4a", "s4b"): + payment = self.env.ref("openupgrade_test_account.ou_outstanding_%s" % name) + self.assertEqual( + payment.state, + "paid", + "ou_outstanding_%s should have been promoted to 'paid'" % name, + )