first commit
This commit is contained in:
commit
302f15fa98
2
__init__.py
Normal file
2
__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import wizards
|
||||
29
__manifest__.py
Normal file
29
__manifest__.py
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
'name': 'Bank Statement Reconciliation',
|
||||
'version': '17.0.1.0.0',
|
||||
'category': 'Accounting',
|
||||
'summary': 'Reconcile bank statement lines with journal entries',
|
||||
'description': """
|
||||
This module allows users to reconcile bank statement lines with journal entries.
|
||||
Features:
|
||||
- Menu to access bank statement lines
|
||||
- Filter by bank journal
|
||||
- Select multiple bank lines to reconcile
|
||||
- Wizard to select journal entries for reconciliation
|
||||
- Automatic creation of reconciliation journal entries
|
||||
""",
|
||||
'author': 'Suherdy Yacob',
|
||||
'depends': [
|
||||
'account',
|
||||
'base',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/bank_statement_line_views.xml',
|
||||
'views/bank_statement_selector_views.xml',
|
||||
'wizards/bank_reconcile_wizard_views.xml',
|
||||
'views/menu.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
3
models/__init__.py
Normal file
3
models/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from . import bank_statement_line
|
||||
from . import bank_statement_selector
|
||||
from . import account_bank_statement_line
|
||||
BIN
models/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
models/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/account_bank_statement_line.cpython-312.pyc
Normal file
BIN
models/__pycache__/account_bank_statement_line.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/bank_statement_line.cpython-312.pyc
Normal file
BIN
models/__pycache__/bank_statement_line.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/bank_statement_selector.cpython-312.pyc
Normal file
BIN
models/__pycache__/bank_statement_selector.cpython-312.pyc
Normal file
Binary file not shown.
35
models/account_bank_statement_line.py
Normal file
35
models/account_bank_statement_line.py
Normal file
@ -0,0 +1,35 @@
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountBankStatementLine(models.Model):
|
||||
_inherit = 'account.bank.statement.line'
|
||||
|
||||
def action_reconcile_selected_lines(self):
|
||||
"""Open the reconciliation wizard for selected lines"""
|
||||
# Get the selected records from the context
|
||||
active_ids = self.env.context.get('active_ids')
|
||||
active_model = self.env.context.get('active_model')
|
||||
|
||||
if active_model == 'account.bank.statement.line' and active_ids:
|
||||
selected_lines = self.browse(active_ids)
|
||||
else:
|
||||
# If called from a single record, use self
|
||||
selected_lines = self
|
||||
|
||||
# Filter out already reconciled lines by checking if they have a move_id with reconciliation in the name
|
||||
unreconciled_lines = selected_lines.filtered(lambda line: not (line.move_id and 'Reconciliation:' in line.move_id.name))
|
||||
|
||||
if not unreconciled_lines:
|
||||
raise UserError("All selected bank statement lines have already been reconciled.")
|
||||
|
||||
return {
|
||||
'name': 'Select Journal Entry to Reconcile',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'bank.reconcile.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_bank_line_ids': unreconciled_lines.ids,
|
||||
}
|
||||
}
|
||||
53
models/bank_statement_line.py
Normal file
53
models/bank_statement_line.py
Normal file
@ -0,0 +1,53 @@
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class BankStatementLine(models.Model):
|
||||
_name = 'bank.statement.line'
|
||||
_description = 'Bank Statement Line Selection'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super().default_get(fields)
|
||||
active_model = self.env.context.get('active_model')
|
||||
active_ids = self.env.context.get('active_ids')
|
||||
|
||||
if active_model == 'account.bank.statement.line' and active_ids:
|
||||
statement_lines = self.env['account.bank.statement.line'].browse(active_ids)
|
||||
res['line_ids'] = [(6, 0, statement_lines.ids)]
|
||||
|
||||
return res
|
||||
|
||||
journal_id = fields.Many2one('account.journal', string='Bank Journal',
|
||||
domain=[('type', '=', 'bank')])
|
||||
line_ids = fields.Many2many('account.bank.statement.line', string='Bank Statement Lines')
|
||||
selected_line_ids = fields.Many2many('account.bank.statement.line',
|
||||
'bank_statement_line_rel',
|
||||
'wizard_id', 'line_id',
|
||||
string='Selected Bank Lines')
|
||||
|
||||
@api.onchange('journal_id')
|
||||
def _onchange_journal_id(self):
|
||||
if self.journal_id:
|
||||
statement_lines = self.env['account.bank.statement.line'].search([
|
||||
('journal_id', '=', self.journal_id.id)
|
||||
])
|
||||
self.line_ids = statement_lines
|
||||
else:
|
||||
self.line_ids = False
|
||||
|
||||
def action_open_reconcile_wizard(self):
|
||||
"""Open the reconciliation wizard"""
|
||||
if not self.selected_line_ids:
|
||||
raise UserError("Please select at least one bank statement line to reconcile.")
|
||||
|
||||
return {
|
||||
'name': 'Select Journal Entry to Reconcile',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'bank.reconcile.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_bank_line_ids': self.selected_line_ids.ids,
|
||||
}
|
||||
}
|
||||
29
models/bank_statement_selector.py
Normal file
29
models/bank_statement_selector.py
Normal file
@ -0,0 +1,29 @@
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class BankStatementSelector(models.TransientModel):
|
||||
_name = 'bank.statement.selector'
|
||||
_description = 'Bank Statement Selector'
|
||||
|
||||
journal_id = fields.Many2one('account.journal',
|
||||
string='Bank Journal',
|
||||
domain=[('type', '=', 'bank')],
|
||||
required=True)
|
||||
|
||||
def action_show_statement_lines(self):
|
||||
"""Open the bank statement lines for the selected journal"""
|
||||
action = {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Bank Statement Lines',
|
||||
'res_model': 'account.bank.statement.line',
|
||||
'view_mode': 'tree,form',
|
||||
'domain': [('journal_id', '=', self.journal_id.id)],
|
||||
'context': {
|
||||
'search_default_journal_id': self.journal_id.id,
|
||||
},
|
||||
'views': [
|
||||
(self.env.ref('bank_statement_reconciliation.view_account_bank_statement_line_tree').id, 'tree'),
|
||||
(self.env.ref('bank_statement_reconciliation.view_account_bank_statement_line_form').id, 'form')
|
||||
]
|
||||
}
|
||||
return action
|
||||
4
security/ir.model.access.csv
Normal file
4
security/ir.model.access.csv
Normal file
@ -0,0 +1,4 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_bank_statement_line,access_bank_statement_line,model_bank_statement_line,account.group_account_user,1,1,1,1
|
||||
access_bank_reconcile_wizard,access_bank_reconcile_wizard,model_bank_reconcile_wizard,account.group_account_user,1,1
|
||||
access_bank_statement_selector,access_bank_statement_selector,model_bank_statement_selector,account.group_account_user,1,1,1,1
|
||||
|
0
views/__init__.py
Normal file
0
views/__init__.py
Normal file
103
views/bank_statement_line_views.xml
Normal file
103
views/bank_statement_line_views.xml
Normal file
@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Action to open the bank statement lines view -->
|
||||
<record id="action_bank_statement_lines" model="ir.actions.act_window">
|
||||
<field name="name">Bank Statement Lines</field>
|
||||
<field name="res_model">account.bank.statement.line</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Select a bank journal to view its statement lines
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Server action for reconciliation -->
|
||||
<record id="action_reconcile_bank_lines" model="ir.actions.server">
|
||||
<field name="name">Reconcile Selected Lines</field>
|
||||
<field name="model_id" ref="account.model_account_bank_statement_line"/>
|
||||
<field name="binding_model_id" ref="account.model_account_bank_statement_line"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
action = {
|
||||
'name': 'Select Journal Entry to Reconcile',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'bank.reconcile.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_bank_line_ids': env.context.get('active_ids'),
|
||||
}
|
||||
}
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Tree view for bank statement lines -->
|
||||
<record id="view_account_bank_statement_line_tree" model="ir.ui.view">
|
||||
<field name="name">account.bank.statement.line.tree</field>
|
||||
<field name="model">account.bank.statement.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Bank Statement Lines" create="0" delete="0" decoration-danger="amount < 0" decoration-muted="move_id and 'Reconciliation:' in move_id.name">
|
||||
<field name="date"/>
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="amount"/>
|
||||
<field name="journal_id"/>
|
||||
<field name="statement_id"/>
|
||||
<field name="move_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form view for bank statement lines -->
|
||||
<record id="view_account_bank_statement_line_form" model="ir.ui.view">
|
||||
<field name="name">account.bank.statement.line.form</field>
|
||||
<field name="model">account.bank.statement.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Bank Statement Line">
|
||||
<header>
|
||||
<button name="action_reconcile_selected_lines" type="object" string="Reconcile" class="btn-primary" invisible="move_id and 'Reconciliation:' in move_id.name"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="date"/>
|
||||
<field name="name"/>
|
||||
<field name="ref"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="amount"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="journal_id" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="statement_id"/>
|
||||
<field name="move_id"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search view for bank statement lines -->
|
||||
<record id="view_account_bank_statement_line_search" model="ir.ui.view">
|
||||
<field name="name">account.bank.statement.line.search</field>
|
||||
<field name="model">account.bank.statement.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="journal_id"/>
|
||||
<field name="date"/>
|
||||
<filter name="positive_amount" string="Income" domain="[('amount', '>', 0)]"/>
|
||||
<filter name="negative_amount" string="Expense" domain="[('amount', '<', 0)]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="group_by_journal" string="Journal" context="{'group_by': 'journal_id'}"/>
|
||||
<filter name="group_by_date" string="Date" context="{'group_by': 'date'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
29
views/bank_statement_selector_views.xml
Normal file
29
views/bank_statement_selector_views.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Form view for bank statement selector -->
|
||||
<record id="view_bank_statement_selector_form" model="ir.ui.view">
|
||||
<field name="name">bank.statement.selector.form</field>
|
||||
<field name="model">bank.statement.selector</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Select Bank Journal">
|
||||
<group>
|
||||
<field name="journal_id" options="{'no_create': True}"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_show_statement_lines" type="object" string="Show Statement Lines"
|
||||
class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action to open the bank statement selector -->
|
||||
<record id="action_bank_statement_selector" model="ir.actions.act_window">
|
||||
<field name="name">Select Bank Journal</field>
|
||||
<field name="res_model">bank.statement.selector</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="view_id" ref="view_bank_statement_selector_form"/>
|
||||
</record>
|
||||
</odoo>
|
||||
14
views/menu.xml
Normal file
14
views/menu.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Menu item for Bank Statement Reconciliation -->
|
||||
<menuitem id="menu_bank_statement_root"
|
||||
name="Bank Reconciliation"
|
||||
sequence="10"
|
||||
groups="account.group_account_user"/>
|
||||
|
||||
<menuitem id="menu_bank_statement_lines"
|
||||
name="Bank Statement Lines"
|
||||
parent="menu_bank_statement_root"
|
||||
action="action_bank_statement_selector"
|
||||
sequence="10"/>
|
||||
</odoo>
|
||||
1
wizards/__init__.py
Normal file
1
wizards/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import bank_reconcile_wizard
|
||||
BIN
wizards/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
wizards/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
wizards/__pycache__/bank_reconcile_wizard.cpython-312.pyc
Normal file
BIN
wizards/__pycache__/bank_reconcile_wizard.cpython-312.pyc
Normal file
Binary file not shown.
131
wizards/bank_reconcile_wizard.py
Normal file
131
wizards/bank_reconcile_wizard.py
Normal file
@ -0,0 +1,131 @@
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class BankReconcileWizard(models.TransientModel):
|
||||
_name = 'bank.reconcile.wizard'
|
||||
_description = 'Bank Reconcile Wizard'
|
||||
|
||||
bank_line_ids = fields.Many2many('account.bank.statement.line',
|
||||
string='Selected Bank Lines',
|
||||
readonly=True)
|
||||
journal_entry_id = fields.Many2one('account.move',
|
||||
string='Journal Entry to Reconcile',
|
||||
domain=[('state', '=', 'posted')])
|
||||
journal_entry_line_id = fields.Many2one('account.move.line',
|
||||
string='Journal Entry Line to Reconcile',
|
||||
domain="[('move_id', '=', journal_entry_id), ('reconciled', '=', False)]")
|
||||
total_bank_line_amount = fields.Float(string='Total Bank Line Amount', compute='_compute_total_bank_line_amount', store=True)
|
||||
|
||||
@api.onchange('journal_entry_id')
|
||||
def _onchange_journal_entry_id(self):
|
||||
"""Reset journal entry line when journal entry changes"""
|
||||
if self.journal_entry_id:
|
||||
self.journal_entry_line_id = False
|
||||
else:
|
||||
self.journal_entry_line_id = False
|
||||
|
||||
|
||||
@api.depends('bank_line_ids')
|
||||
def _compute_total_bank_line_amount(self):
|
||||
"""Compute total amount of selected bank lines"""
|
||||
for record in self:
|
||||
record.total_bank_line_amount = sum(line.amount for line in record.bank_line_ids)
|
||||
|
||||
def action_reconcile(self):
|
||||
"""Perform the reconciliation for each selected bank line"""
|
||||
if not self.journal_entry_id:
|
||||
raise UserError("Please select a journal entry to reconcile.")
|
||||
|
||||
if not self.journal_entry_line_id:
|
||||
raise UserError("Please select a journal entry line to reconcile.")
|
||||
|
||||
# Validate that the total of individual line amounts doesn't exceed the journal entry line amount
|
||||
journal_line_amount = self.journal_entry_line_id.debit or self.journal_entry_line_id.credit
|
||||
if len(self.bank_line_ids) > 1:
|
||||
total_bank_amount = sum(abs(line.amount) for line in self.bank_line_ids)
|
||||
if total_bank_amount > journal_line_amount:
|
||||
raise UserError(f"Total bank line amounts ({total_bank_amount}) cannot exceed the journal entry line amount ({journal_line_amount}).")
|
||||
|
||||
# Process each selected bank line individually
|
||||
for bank_line in self.bank_line_ids:
|
||||
self._reconcile_single_line(bank_line, self.journal_entry_line_id)
|
||||
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def _reconcile_single_line(self, bank_line, journal_entry_line):
|
||||
"""Reconcile a single bank line with a journal entry line"""
|
||||
# Get the account from the journal entry line that will be on the opposite side
|
||||
reconcile_account = journal_entry_line.account_id
|
||||
|
||||
# Use the individual bank line amount for each line
|
||||
reconcile_amount = abs(bank_line.amount)
|
||||
|
||||
# According to requirement #5: if the bank line is debit,
|
||||
# then the new journal entry after reconcile will have that bank account in debit
|
||||
bank_account = bank_line.journal_id.default_account_id
|
||||
bank_line_amount = bank_line.amount
|
||||
|
||||
# Determine if the bank line is debit (positive) or credit (negative)
|
||||
if bank_line_amount >= 0: # Bank line is debit (money coming in)
|
||||
# Bank account should be debited (requirement #5)
|
||||
debit_account = bank_account
|
||||
# The account from the selected journal entry line goes to credit (requirement #6)
|
||||
credit_account = reconcile_account
|
||||
else: # Bank line is credit (money going out)
|
||||
# Bank account should be credited
|
||||
credit_account = bank_account
|
||||
# The account from the selected journal entry line goes to debit
|
||||
debit_account = reconcile_account
|
||||
|
||||
# Set the amounts - make sure they are balanced
|
||||
debit_amount = reconcile_amount
|
||||
credit_amount = reconcile_amount
|
||||
|
||||
# Create a new journal entry for reconciliation with lines
|
||||
reconciling_move = self.env['account.move'].create({
|
||||
'journal_id': bank_line.journal_id.id,
|
||||
'date': bank_line.date,
|
||||
'ref': f'Reconciliation: {bank_line.name or "Bank Line"}',
|
||||
'move_type': 'entry',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'account_id': debit_account.id,
|
||||
'debit': debit_amount,
|
||||
'credit': 0,
|
||||
'name': f'Bank Reconciliation: {bank_line.name or ""}',
|
||||
}),
|
||||
(0, 0, {
|
||||
'account_id': credit_account.id,
|
||||
'debit': 0,
|
||||
'credit': credit_amount,
|
||||
'name': f'Bank Reconciliation: {bank_line.name or ""}',
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
# Post the reconciling journal entry
|
||||
reconciling_move.action_post()
|
||||
|
||||
# Link the bank line to the reconciling move
|
||||
bank_line.sudo().write({
|
||||
'move_id': reconciling_move.id,
|
||||
})
|
||||
|
||||
# Mark the bank line as reconciled in a separate operation
|
||||
bank_line.sudo().write({
|
||||
'is_reconciled': True,
|
||||
})
|
||||
|
||||
# Try to reconcile the selected journal entry line with the corresponding line in the reconciling move
|
||||
# Find the line in the reconciling move that has the same account as the journal entry line
|
||||
reconciling_line = reconciling_move.line_ids.filtered(lambda l: l.account_id.id == reconcile_account.id and l.credit > 0)
|
||||
|
||||
# Try to reconcile the journal entry line with our reconciling line
|
||||
if reconciling_line:
|
||||
try:
|
||||
(journal_entry_line + reconciling_line).reconcile()
|
||||
except:
|
||||
# If reconcile fails, we'll just link the moves
|
||||
pass
|
||||
36
wizards/bank_reconcile_wizard_views.xml
Normal file
36
wizards/bank_reconcile_wizard_views.xml
Normal file
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Wizard form view -->
|
||||
<record id="view_bank_reconcile_wizard_form" model="ir.ui.view">
|
||||
<field name="name">bank.reconcile.wizard.form</field>
|
||||
<field name="model">bank.reconcile.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Reconcile Bank Lines">
|
||||
<group>
|
||||
<field name="bank_line_ids" widget="many2many" options="{'no_create': True}" readonly="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="total_bank_line_amount" readonly="1"/>
|
||||
<field name="journal_entry_id" options="{'no_create': True}"/>
|
||||
<field name="journal_entry_line_id"
|
||||
domain="[('move_id', '=', journal_entry_id), ('reconciled', '=', False)]"
|
||||
invisible="journal_entry_id == False"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_reconcile" type="object" string="Reconcile"
|
||||
class="btn-primary"
|
||||
invisible="journal_entry_id == False or journal_entry_line_id == False"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action to open the wizard -->
|
||||
<record id="action_bank_reconcile_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Select Journal Entry to Reconcile</field>
|
||||
<field name="res_model">bank.reconcile.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue
Block a user