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
130 changes: 130 additions & 0 deletions account_payment_internal_transfer/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
=================================
Account Payment Internal Transfer
=================================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:ac4763f2fc97bb68312bd7af310daff3a8b959e09ef02fc28a456daae89ebec5
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--payment-lightgray.png?logo=github
:target: https://github.com/OCA/account-payment/tree/18.0/account_payment_internal_transfer
:alt: OCA/account-payment
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/account-payment-18-0/account-payment-18-0-account_payment_internal_transfer
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/account-payment&target_branch=18.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module restores the internal transfer functionality for payments
that was available in previous Odoo versions but was removed.

Now the recommended approach for internal transfers requires waiting for
bank statement lines to be imported and then using reconciliation
models. This workflow has significant drawbacks:

- Users cannot register transfers at the moment they occur
- There is a risk of forgetting unreconciled transactions if bank
statements are delayed
- Users without direct bank access have no way to track pending
transfers
- The workflow is tedious and error-prone

This module allows users to register internal transfers immediately when
they happen, creating the intermediate journal entries with outstanding
accounts. Later, when bank statements are imported, each side can be
reconciled independently. This results in 4 journal entries total (2
payments + 2 bank statement reconciliations).

**Features:**

- Create internal transfers directly from the Accounting Dashboard
- A paired payment is automatically created in the destination journal
- Their journal entries are automatically reconciled
- Proper labels are set on journal items for easy identification
- Cancel and reset to draft cascades to the paired payment
- Deleting a transfer also deletes the paired payment
- Search filter to easily find internal transfers in the payments list

**Table of contents**

.. contents::
:local:

Usage
=====

To create an internal transfer:

1. Go to Accounting Dashboard
2. Click on the menu (three dots) of a Bank or Cash journal
3. In the "New" section, click on "Internal Transfer"
4. Select the destination journal
5. Enter the amount
6. Confirm the payment

The system will automatically:

- Create a paired payment in the destination journal
- Use the company's transfer account for the counterpart
- Reconcile both transfer lines automatically

To find existing transfers, use the "Internal Transfers" filter in the
payments search view.

When cancelling, resetting to draft, or deleting one side of the
transfer, the paired payment is automatically updated as well.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-payment/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/account-payment/issues/new?body=module:%20account_payment_internal_transfer%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* ForgeFlow

Contributors
------------

- `ForgeFlow <https://www.forgeflow.com>`__:

- Joan Sisquella <joan.sisquella@forgeflow.com>

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/account-payment <https://github.com/OCA/account-payment/tree/18.0/account_payment_internal_transfer>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
4 changes: 4 additions & 0 deletions account_payment_internal_transfer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2026 ForgeFlow S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).

from . import models
20 changes: 20 additions & 0 deletions account_payment_internal_transfer/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2026 ForgeFlow S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
{
"name": "Account Payment Internal Transfer",
"summary": "Restore internal transfer functionality between bank/cash journals",
"version": "18.0.1.0.0",
"development_status": "Beta",
"category": "Accounting/Accounting",
"website": "https://github.com/OCA/account-payment",
"author": "ForgeFlow, Odoo Community Association (OCA)",
"license": "LGPL-3",
"installable": True,
"depends": [
"account",
],
"data": [
"views/account_journal_dashboard_view.xml",
"views/account_payment_views.xml",
],
}
5 changes: 5 additions & 0 deletions account_payment_internal_transfer/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copyright 2026 ForgeFlow S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).

from . import account_journal
from . import account_payment
24 changes: 24 additions & 0 deletions account_payment_internal_transfer/models/account_journal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright 2026 ForgeFlow S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).

from odoo import models


class AccountJournal(models.Model):
_inherit = "account.journal"

def create_internal_transfer(self):
action = self.env["ir.actions.actions"]._for_xml_id(
"account.action_account_payments"
)
action.update(
{
"views": [[False, "form"]],
"context": {
"default_journal_id": self.id,
"default_payment_type": "outbound",
"default_is_internal_transfer": True,
},
}
)
return action
177 changes: 177 additions & 0 deletions account_payment_internal_transfer/models/account_payment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Copyright 2026 ForgeFlow S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).

from odoo import _, api, fields, models
from odoo.exceptions import UserError


class AccountPayment(models.Model):
_inherit = "account.payment"

is_internal_transfer = fields.Boolean(
string="Internal Transfer",
tracking=True,
)
destination_journal_id = fields.Many2one(
comodel_name="account.journal",
string="Destination Journal",
domain="[('type', 'in', ('bank', 'cash')), "
"('company_id', '=', company_id), "
"('id', '!=', journal_id)]",
check_company=True,
)

@api.depends("journal_id", "partner_id", "partner_type", "is_internal_transfer")
def _compute_destination_account_id(self):
internal_transfers = self.filtered("is_internal_transfer")
regular_payments = self - internal_transfers
if regular_payments:
super(AccountPayment, regular_payments)._compute_destination_account_id()
for payment in internal_transfers:
payment.destination_account_id = (
payment.journal_id.company_id.transfer_account_id
)
return

@api.depends(
"partner_id",
"company_id",
"payment_type",
"destination_journal_id",
"is_internal_transfer",
)
def _compute_available_partner_bank_ids(self):
internal_transfers = self.filtered("is_internal_transfer")
regular_payments = self - internal_transfers
if regular_payments:
super(
AccountPayment, regular_payments
)._compute_available_partner_bank_ids()
for payment in internal_transfers:
if payment.payment_type == "inbound":
payment.available_partner_bank_ids = payment.journal_id.bank_account_id
else:
payment.available_partner_bank_ids = (
payment.destination_journal_id.bank_account_id
)
return

def action_cancel(self):
res = super().action_cancel()
for payment in self:
paired = payment.paired_internal_transfer_payment_id
if paired and paired.state != "canceled":
paired.action_cancel()
return res

def action_draft(self):
res = super().action_draft()
for payment in self:
paired = payment.paired_internal_transfer_payment_id
if paired and paired.state != "draft":
paired.action_draft()
return res

def unlink(self):
non_draft = self.filtered(
lambda p: p.is_internal_transfer and p.state != "draft"
)
if non_draft:
raise UserError(
_(
"You can only delete internal transfers in draft state. "
"Please reset to draft first."
)
)
paired = self.mapped("paired_internal_transfer_payment_id") - self
res = super().unlink()
if paired.exists():
paired.unlink()
return res

def button_open_paired_payment(self):
self.ensure_one()
return {
"name": _("Paired Payment"),
"type": "ir.actions.act_window",
"res_model": "account.payment",
"view_mode": "form",
"res_id": self.paired_internal_transfer_payment_id.id,
}

def action_post(self):
res = super().action_post()
self.filtered(
lambda p: p.is_internal_transfer
and not p.paired_internal_transfer_payment_id
)._create_paired_internal_transfer_payment()
return res

def _create_paired_internal_transfer_payment(self):
for payment in self:
paired_payment = self.create(
payment._prepare_paired_internal_transfer_values()
)
paired_payment.action_post()
payment.paired_internal_transfer_payment_id = paired_payment
body = _(
"This payment has been created from: %(payment)s",
payment=payment._get_html_link(),
)
paired_payment.message_post(body=body)
body = _(
"A paired payment has been created: %(payment)s",
payment=paired_payment._get_html_link(),
)
payment.message_post(body=body)
(payment + paired_payment)._reconcile_internal_transfer_lines()

def _reconcile_internal_transfer_lines(self):
payments_with_move = self.filtered("move_id")
if len(payments_with_move) < 2:
return
transfer_account = payments_with_move[0].destination_account_id
lines = payments_with_move.move_id.line_ids.filtered(
lambda line: line.account_id == transfer_account and not line.reconciled
)
if lines:
lines.reconcile()

def _prepare_paired_internal_transfer_values(self):
self.ensure_one()
paired_payment_type = (
"inbound" if self.payment_type == "outbound" else "outbound"
)
return {
"journal_id": self.destination_journal_id.id,
"amount": self.amount,
"partner_id": self.company_id.partner_id.id,
"currency_id": self.currency_id.id,
"destination_journal_id": self.journal_id.id,
"payment_type": paired_payment_type,
"memo": self.memo or _("Internal Transfer"),
"paired_internal_transfer_payment_id": self.id,
"date": self.date,
"is_internal_transfer": True,
}

def _prepare_move_line_default_vals(
self, write_off_line_vals=None, force_balance=None
):
line_vals_list = super()._prepare_move_line_default_vals(
write_off_line_vals=write_off_line_vals,
force_balance=force_balance,
)
if self.is_internal_transfer and line_vals_list:
if self.payment_type == "outbound":
liquidity_line_name = _(
"Transfer to %s", self.destination_journal_id.name
)
else:
liquidity_line_name = _(
"Transfer from %s", self.destination_journal_id.name
)
line_vals_list[0]["name"] = liquidity_line_name
if len(line_vals_list) > 1:
line_vals_list[1]["name"] = _("Internal Transfer")
return line_vals_list
5 changes: 5 additions & 0 deletions account_payment_internal_transfer/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[project]
name = "odoo-addon-account_payment_internal_transfer"
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
3 changes: 3 additions & 0 deletions account_payment_internal_transfer/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- [ForgeFlow](https://www.forgeflow.com):

- Joan Sisquella \<joan.sisquella@forgeflow.com\>
Loading
Loading