commit 302f15fa98c595a6a9ba4e4c467335f8e74c8be1 Author: Suherdy Yacob Date: Sat Oct 25 21:44:56 2025 +0700 first commit diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..6ed2c21 --- /dev/null +++ b/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards \ No newline at end of file diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..4e8a068 --- /dev/null +++ b/__manifest__.py @@ -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, +} \ No newline at end of file diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..944fd2b Binary files /dev/null and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..96b9f9b --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,3 @@ +from . import bank_statement_line +from . import bank_statement_selector +from . import account_bank_statement_line \ No newline at end of file diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..f1d675a Binary files /dev/null and b/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/models/__pycache__/account_bank_statement_line.cpython-312.pyc b/models/__pycache__/account_bank_statement_line.cpython-312.pyc new file mode 100644 index 0000000..1d026d5 Binary files /dev/null and b/models/__pycache__/account_bank_statement_line.cpython-312.pyc differ diff --git a/models/__pycache__/bank_statement_line.cpython-312.pyc b/models/__pycache__/bank_statement_line.cpython-312.pyc new file mode 100644 index 0000000..031b0b4 Binary files /dev/null and b/models/__pycache__/bank_statement_line.cpython-312.pyc differ diff --git a/models/__pycache__/bank_statement_selector.cpython-312.pyc b/models/__pycache__/bank_statement_selector.cpython-312.pyc new file mode 100644 index 0000000..3c3326f Binary files /dev/null and b/models/__pycache__/bank_statement_selector.cpython-312.pyc differ diff --git a/models/account_bank_statement_line.py b/models/account_bank_statement_line.py new file mode 100644 index 0000000..60cfdba --- /dev/null +++ b/models/account_bank_statement_line.py @@ -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, + } + } \ No newline at end of file diff --git a/models/bank_statement_line.py b/models/bank_statement_line.py new file mode 100644 index 0000000..2379e58 --- /dev/null +++ b/models/bank_statement_line.py @@ -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, + } + } \ No newline at end of file diff --git a/models/bank_statement_selector.py b/models/bank_statement_selector.py new file mode 100644 index 0000000..9913086 --- /dev/null +++ b/models/bank_statement_selector.py @@ -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 \ No newline at end of file diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv new file mode 100644 index 0000000..d8cc973 --- /dev/null +++ b/security/ir.model.access.csv @@ -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 \ No newline at end of file diff --git a/views/__init__.py b/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/views/bank_statement_line_views.xml b/views/bank_statement_line_views.xml new file mode 100644 index 0000000..5e79a77 --- /dev/null +++ b/views/bank_statement_line_views.xml @@ -0,0 +1,103 @@ + + + + + Bank Statement Lines + account.bank.statement.line + tree,form + +

+ Select a bank journal to view its statement lines +

+
+
+ + + + Reconcile Selected Lines + + + 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'), + } + } + + + + + + account.bank.statement.line.tree + account.bank.statement.line + + + + + + + + + + + + + + + + account.bank.statement.line.form + account.bank.statement.line + +
+
+
+ + + + + + + + + + + + + + + + + + + +
+
+
+ + + + account.bank.statement.line.search + account.bank.statement.line + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/views/bank_statement_selector_views.xml b/views/bank_statement_selector_views.xml new file mode 100644 index 0000000..407e2cc --- /dev/null +++ b/views/bank_statement_selector_views.xml @@ -0,0 +1,29 @@ + + + + + bank.statement.selector.form + bank.statement.selector + +
+ + + +
+
+
+
+
+ + + + Select Bank Journal + bank.statement.selector + form + new + + +
\ No newline at end of file diff --git a/views/menu.xml b/views/menu.xml new file mode 100644 index 0000000..87eb218 --- /dev/null +++ b/views/menu.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/wizards/__init__.py b/wizards/__init__.py new file mode 100644 index 0000000..a9869ef --- /dev/null +++ b/wizards/__init__.py @@ -0,0 +1 @@ +from . import bank_reconcile_wizard \ No newline at end of file diff --git a/wizards/__pycache__/__init__.cpython-312.pyc b/wizards/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..a9ec53d Binary files /dev/null and b/wizards/__pycache__/__init__.cpython-312.pyc differ diff --git a/wizards/__pycache__/bank_reconcile_wizard.cpython-312.pyc b/wizards/__pycache__/bank_reconcile_wizard.cpython-312.pyc new file mode 100644 index 0000000..1cf366f Binary files /dev/null and b/wizards/__pycache__/bank_reconcile_wizard.cpython-312.pyc differ diff --git a/wizards/bank_reconcile_wizard.py b/wizards/bank_reconcile_wizard.py new file mode 100644 index 0000000..c9bddfd --- /dev/null +++ b/wizards/bank_reconcile_wizard.py @@ -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 \ No newline at end of file diff --git a/wizards/bank_reconcile_wizard_views.xml b/wizards/bank_reconcile_wizard_views.xml new file mode 100644 index 0000000..ee25624 --- /dev/null +++ b/wizards/bank_reconcile_wizard_views.xml @@ -0,0 +1,36 @@ + + + + + bank.reconcile.wizard.form + bank.reconcile.wizard + +
+ + + + + + + + +
+
+
+
+
+ + + + Select Journal Entry to Reconcile + bank.reconcile.wizard + form + new + +
\ No newline at end of file