first commit
This commit is contained in:
commit
4fbc0f17d8
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Python
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
# Odoo
|
||||||
|
*.pot
|
||||||
|
*.po
|
||||||
|
*.csv~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
32
README.md
Normal file
32
README.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Custom Bank Internal Transfer
|
||||||
|
|
||||||
|
A custom Odoo 17 module that provides a simplified 2-entry internal bank transfer flow.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* **Dedicated Menu**: Adds a separate "Bank Internal Transfer" menu under Accounting/Vendors.
|
||||||
|
* **Direct Reconciliation Flow**: Generates only 2 journal entries instead of the standard 3, completely bypassing the liquidity transfer / internal transfer account.
|
||||||
|
* Entry 1: Source Bank Statement Line (Dr Outstanding Payments [Source], Cr Source Bank)
|
||||||
|
* Entry 2: Destination Bank Statement Line (Dr Dest Bank, Cr Outstanding Payments [Source])
|
||||||
|
* **Unique Sequencing**: Implements a dedicated sequence (e.g. `BIT/2026/0001`) for bank internal transfers.
|
||||||
|
* **Printable Receipt**: Supports generating PDF payment receipts using the standard vendor payment receipt template.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
* `account`
|
||||||
|
* `account_accountant` (Required for Odoo Enterprise accounting menus)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Copy the `account_custom_internal_transfer` directory to your Odoo custom addons path.
|
||||||
|
2. Update the App List in Odoo.
|
||||||
|
3. Install the "Custom Bank Internal Transfer" module.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Navigate to **Accounting > Vendors > Bank Internal Transfer**.
|
||||||
|
2. Click **New**.
|
||||||
|
3. Select the **Source Journal** (e.g., Bank BCA) and **Destination Journal** (e.g., Cash).
|
||||||
|
4. Specify the **Date**, **Amount**, and **Memo**.
|
||||||
|
5. Click **Confirm** to execute the transfer.
|
||||||
|
6. To print a receipt, click **Print > Payment Receipt**.
|
||||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
22
__manifest__.py
Normal file
22
__manifest__.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
'name': 'Custom Bank Internal Transfer',
|
||||||
|
'version': '19.0.1.0',
|
||||||
|
'category': 'Accounting/Accounting',
|
||||||
|
'summary': 'Simplified 2-entry internal bank transfers',
|
||||||
|
'description': """
|
||||||
|
Creates a new menu for Bank Internal Transfers.
|
||||||
|
Generates 2 journal entries instead of 3, completely bypassing the liquidity transfer account.
|
||||||
|
Entry 1: Source Bank Stmt Line (Dr Outstanding Payments [Source], Cr Source Bank)
|
||||||
|
Entry 2: Destination Bank Stmt Line (Dr Dest Bank, Cr Outstanding Payments [Source])
|
||||||
|
""",
|
||||||
|
'author': 'Suherdy Yacob',
|
||||||
|
'depends': ['account', 'account_accountant'],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'data/ir_sequence_data.xml',
|
||||||
|
'views/bank_internal_transfer_views.xml',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
12
data/ir_sequence_data.xml
Normal file
12
data/ir_sequence_data.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="seq_bank_internal_transfer" model="ir.sequence">
|
||||||
|
<field name="name">Bank Internal Transfer Sequence</field>
|
||||||
|
<field name="code">bank.internal.transfer</field>
|
||||||
|
<field name="prefix">BIT/%(year)s/</field>
|
||||||
|
<field name="padding">5</field>
|
||||||
|
<field name="company_id" eval="False"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
1
models/__init__.py
Normal file
1
models/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import bank_internal_transfer
|
||||||
141
models/bank_internal_transfer.py
Normal file
141
models/bank_internal_transfer.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
from odoo import models, fields, api, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
class BankInternalTransfer(models.Model):
|
||||||
|
_name = 'bank.internal.transfer'
|
||||||
|
_description = 'Bank Internal Transfer'
|
||||||
|
_order = 'date desc, id desc'
|
||||||
|
|
||||||
|
name = fields.Char(string='Reference', required=True, copy=False, readonly=True, default=lambda self: _('New'))
|
||||||
|
source_journal_id = fields.Many2one('account.journal', string='Source Bank Journal', domain="[('type', '=', 'bank')]", required=True)
|
||||||
|
destination_journal_id = fields.Many2one('account.journal', string='Destination Bank Journal', domain="[('type', '=', 'bank')]", required=True)
|
||||||
|
date = fields.Date(string='Date', required=True, default=fields.Date.context_today)
|
||||||
|
amount = fields.Monetary(string='Amount', required=True)
|
||||||
|
currency_id = fields.Many2one('res.currency', compute='_compute_currency_id', store=True)
|
||||||
|
company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company)
|
||||||
|
memo = fields.Char(string='Memo')
|
||||||
|
payment_method_line_id = fields.Many2one('account.payment.method.line', string='Payment Method', domain="[('journal_id', '=', source_journal_id), ('payment_type', '=', 'outbound')]")
|
||||||
|
state = fields.Selection([
|
||||||
|
('draft', 'Draft'),
|
||||||
|
('posted', 'Posted'),
|
||||||
|
], string='Status', default='draft', required=True)
|
||||||
|
statement_line_id = fields.Many2one('account.bank.statement.line', string='Source Statement Line', readonly=True)
|
||||||
|
|
||||||
|
# Fields for report compatibility (account.report_payment_receipt)
|
||||||
|
ref = fields.Char(string='Report Reference', related='memo')
|
||||||
|
partner_id = fields.Many2one('res.partner', compute='_compute_dummy_fields')
|
||||||
|
partner_type = fields.Char(compute='_compute_dummy_fields')
|
||||||
|
payment_method_id = fields.Many2one('account.payment.method', compute='_compute_payment_method_id')
|
||||||
|
reconciled_invoice_ids = fields.Many2many('account.move', compute='_compute_dummy_fields')
|
||||||
|
reconciled_bill_ids = fields.Many2many('account.move', compute='_compute_dummy_fields')
|
||||||
|
journal_id = fields.Many2one('account.journal', compute='_compute_dummy_fields')
|
||||||
|
partner_bank_id = fields.Many2one('res.partner.bank', compute='_compute_dummy_fields')
|
||||||
|
payment_ids = fields.Many2many('bank.internal.transfer', compute='_compute_dummy_fields')
|
||||||
|
deduction_line_ids = fields.Many2many('account.move', compute='_compute_dummy_fields')
|
||||||
|
ref = fields.Char(related='memo')
|
||||||
|
amount_total = fields.Monetary(related='amount')
|
||||||
|
final_payment_amount = fields.Monetary(related='amount')
|
||||||
|
|
||||||
|
def _compute_dummy_fields(self):
|
||||||
|
for rec in self:
|
||||||
|
# Mimic standard Odoo internal transfer behavior where the company is the partner
|
||||||
|
rec.partner_id = rec.company_id.partner_id
|
||||||
|
rec.partner_type = 'supplier'
|
||||||
|
rec.reconciled_invoice_ids = False
|
||||||
|
rec.reconciled_bill_ids = False
|
||||||
|
rec.journal_id = rec.source_journal_id
|
||||||
|
rec.partner_bank_id = rec.destination_journal_id.bank_account_id
|
||||||
|
rec.payment_ids = rec.ids
|
||||||
|
rec.deduction_line_ids = False
|
||||||
|
|
||||||
|
@api.depends('payment_method_line_id')
|
||||||
|
def _compute_payment_method_id(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.payment_method_id = rec.payment_method_line_id.payment_method_id
|
||||||
|
|
||||||
|
def _get_payment_receipt_report_values(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'display_payment_method': True,
|
||||||
|
'display_invoices': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api.depends('source_journal_id')
|
||||||
|
def _compute_currency_id(self):
|
||||||
|
for record in self:
|
||||||
|
record.currency_id = record.source_journal_id.currency_id or record.company_id.currency_id
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
for vals in vals_list:
|
||||||
|
if vals.get('name', _('New')) == _('New'):
|
||||||
|
vals['name'] = self.env['ir.sequence'].next_by_code('bank.internal.transfer') or _('New')
|
||||||
|
return super().create(vals_list)
|
||||||
|
|
||||||
|
def action_confirm(self):
|
||||||
|
for transfer in self:
|
||||||
|
if transfer.amount <= 0:
|
||||||
|
raise UserError(_("Amount must be strictly positive."))
|
||||||
|
if transfer.source_journal_id == transfer.destination_journal_id:
|
||||||
|
raise UserError(_("Source and destination journals must be different."))
|
||||||
|
|
||||||
|
# 1. Determine Outstanding Payments account from Source Journal
|
||||||
|
outstanding_account = transfer.source_journal_id.outbound_payment_method_line_ids.mapped('payment_account_id')
|
||||||
|
if outstanding_account:
|
||||||
|
outstanding_account = outstanding_account[0]
|
||||||
|
else:
|
||||||
|
outstanding_account = transfer.company_id.account_journal_payment_credit_account_id
|
||||||
|
|
||||||
|
if not outstanding_account:
|
||||||
|
raise UserError(_("Please configure the Outstanding Payments Account on the source journal or company."))
|
||||||
|
|
||||||
|
# 2. Create the Bank Statement Line for the Source Journal
|
||||||
|
# This generates the `account.move` with Credit Bank, Debit Suspense
|
||||||
|
stmt_line_vals = {
|
||||||
|
'journal_id': transfer.source_journal_id.id,
|
||||||
|
'date': transfer.date,
|
||||||
|
'payment_ref': transfer.memo or transfer.name,
|
||||||
|
'amount': -transfer.amount, # Outgoing money
|
||||||
|
}
|
||||||
|
stmt_line = self.env['account.bank.statement.line'].create(stmt_line_vals)
|
||||||
|
|
||||||
|
# 3. Modify the underlying move's suspense line to use the Outstanding Payments account
|
||||||
|
suspense_account = transfer.source_journal_id.suspense_account_id
|
||||||
|
if not suspense_account:
|
||||||
|
raise UserError(_("Please configure the Suspense Account on the source journal."))
|
||||||
|
|
||||||
|
move = stmt_line.move_id
|
||||||
|
if move.state != 'draft':
|
||||||
|
move.button_draft()
|
||||||
|
|
||||||
|
suspense_line = move.line_ids.filtered(lambda l: l.account_id == suspense_account)
|
||||||
|
if not suspense_line:
|
||||||
|
# Fallback if somehow it didn't use the suspense account
|
||||||
|
suspense_line = move.line_ids.filtered(lambda l: l.debit > 0)
|
||||||
|
|
||||||
|
if suspense_line:
|
||||||
|
# We use check_move_validity=False to bypass balance checks while updating
|
||||||
|
suspense_line.with_context(check_move_validity=False).write({
|
||||||
|
'account_id': outstanding_account.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Post the move
|
||||||
|
move.action_post()
|
||||||
|
|
||||||
|
# Link and update state
|
||||||
|
transfer.write({
|
||||||
|
'statement_line_id': stmt_line.id,
|
||||||
|
'state': 'posted',
|
||||||
|
})
|
||||||
|
|
||||||
|
def action_draft(self):
|
||||||
|
for transfer in self:
|
||||||
|
if transfer.statement_line_id:
|
||||||
|
# Check if it's already reconciled on the destination side
|
||||||
|
for line in transfer.statement_line_id.move_id.line_ids:
|
||||||
|
if line.reconciled:
|
||||||
|
raise UserError(_("You cannot reset a transfer that has already been reconciled on the destination side."))
|
||||||
|
|
||||||
|
transfer.statement_line_id.move_id.button_draft()
|
||||||
|
transfer.statement_line_id.unlink()
|
||||||
|
transfer.state = 'draft'
|
||||||
3
security/ir.model.access.csv
Normal file
3
security/ir.model.access.csv
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_bank_internal_transfer_user,bank.internal.transfer.user,model_bank_internal_transfer,account.group_account_user,1,1,1,1
|
||||||
|
access_bank_internal_transfer_manager,bank.internal.transfer.manager,model_bank_internal_transfer,account.group_account_manager,1,1,1,1
|
||||||
|
109
views/bank_internal_transfer_views.xml
Normal file
109
views/bank_internal_transfer_views.xml
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Sequence for the custom internal transfer -->
|
||||||
|
<record id="seq_bank_internal_transfer" model="ir.sequence">
|
||||||
|
<field name="name">Bank Internal Transfer Sequence</field>
|
||||||
|
<field name="code">bank.internal.transfer</field>
|
||||||
|
<field name="prefix">INT/TRANS/</field>
|
||||||
|
<field name="padding">4</field>
|
||||||
|
<field name="company_id" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Tree View -->
|
||||||
|
<record id="view_bank_internal_transfer_tree" model="ir.ui.view">
|
||||||
|
<field name="name">bank.internal.transfer.tree</field>
|
||||||
|
<field name="model">bank.internal.transfer</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Bank Internal Transfers">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="source_journal_id"/>
|
||||||
|
<field name="destination_journal_id"/>
|
||||||
|
<field name="amount" sum="Total"/>
|
||||||
|
<field name="currency_id" column_invisible="True"/>
|
||||||
|
<field name="state" widget="badge" decoration-info="state == 'draft'" decoration-success="state == 'posted'"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Form View -->
|
||||||
|
<record id="view_bank_internal_transfer_form" model="ir.ui.view">
|
||||||
|
<field name="name">bank.internal.transfer.form</field>
|
||||||
|
<field name="model">bank.internal.transfer</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Bank Internal Transfer">
|
||||||
|
<header>
|
||||||
|
<button name="action_confirm" type="object" string="Confirm" class="oe_highlight" invisible="state != 'draft'"/>
|
||||||
|
<button name="action_draft" type="object" string="Reset to Draft" invisible="state != 'posted'"/>
|
||||||
|
<field name="state" widget="statusbar" statusbar_visible="draft,posted"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name" readonly="1"/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="source_journal_id" readonly="state != 'draft'"/>
|
||||||
|
<field name="destination_journal_id" readonly="state != 'draft'"/>
|
||||||
|
<field name="payment_method_line_id" readonly="state != 'draft'"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="date" readonly="state != 'draft'"/>
|
||||||
|
<field name="amount" readonly="state != 'draft'"/>
|
||||||
|
<field name="currency_id" invisible="1"/>
|
||||||
|
<field name="memo" readonly="state != 'draft'"/>
|
||||||
|
<field name="company_id" invisible="1"/>
|
||||||
|
<field name="statement_line_id" invisible="not statement_line_id"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action -->
|
||||||
|
<record id="action_bank_internal_transfer" model="ir.actions.act_window">
|
||||||
|
<field name="name">Bank Internal Transfers</field>
|
||||||
|
<field name="res_model">bank.internal.transfer</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Create your first Bank Internal Transfer
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Transfer funds directly between bank accounts with a simplified 2-entry process.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Report Template for Internal Transfer (delegates to standard payment document) -->
|
||||||
|
<template id="report_bank_internal_transfer_receipt">
|
||||||
|
<t t-call="web.html_container">
|
||||||
|
<t t-foreach="docs" t-as="o">
|
||||||
|
<!-- Fallback to company lang since partner_id might be empty/company itself -->
|
||||||
|
<t t-set="lang" t-value="o.company_id.partner_id.lang"/>
|
||||||
|
<t t-call="account.report_payment_receipt_document" t-lang="lang"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Report Action -->
|
||||||
|
<record id="action_report_bank_internal_transfer_receipt" model="ir.actions.report">
|
||||||
|
<field name="name">Payment Receipt</field>
|
||||||
|
<field name="model">bank.internal.transfer</field>
|
||||||
|
<field name="report_type">qweb-pdf</field>
|
||||||
|
<field name="report_name">account_custom_internal_transfer.report_bank_internal_transfer_receipt</field>
|
||||||
|
<field name="report_file">account_custom_internal_transfer.report_bank_internal_transfer_receipt</field>
|
||||||
|
<field name="binding_model_id" ref="model_bank_internal_transfer"/>
|
||||||
|
<field name="binding_type">report</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menu Item in Accounting App Top Level -->
|
||||||
|
<menuitem id="menu_bank_internal_transfer"
|
||||||
|
name="Bank Internal Transfer"
|
||||||
|
parent="account_accountant.menu_accounting"
|
||||||
|
action="action_bank_internal_transfer"
|
||||||
|
sequence="25"/>
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in New Issue
Block a user