feat: Implement bank statement line selection with total amount display in footer and reconciliation wizard.

This commit is contained in:
admin.suherdy 2025-12-06 19:13:17 +07:00
parent 5dfc280ed6
commit d75a96007e
28 changed files with 882 additions and 891 deletions

513
README.md
View File

@ -1,262 +1,253 @@
# Bank Statement Reconciliation Module # Bank Statement Reconciliation Module
## Overview ## Overview
This module enhances Odoo's bank statement reconciliation functionality by providing an intuitive interface for reconciling bank statement lines with journal entries. This module enhances Odoo's bank statement reconciliation functionality by providing an intuitive interface for reconciling bank statement lines with journal entries.
## Features ## Features
### Core Functionality ### Core Functionality
- **Menu Access**: Direct menu to access bank statement lines - **Menu Access**: Direct menu to access bank statement lines
- **Bank Journal Filtering**: Filter statement lines by specific bank journals - **Bank Journal Filtering**: Filter statement lines by specific bank journals
- **Batch Reconciliation**: Select and reconcile multiple bank lines simultaneously - **Batch Reconciliation**: Select and reconcile multiple bank lines simultaneously
- **Journal Entry Matching**: Wizard to select appropriate journal entries for reconciliation - **Journal Entry Matching**: Wizard to select appropriate journal entries for reconciliation
- **Automatic Reconciliation**: Automatic creation of reconciliation journal entries - **Automatic Reconciliation**: Automatic creation of reconciliation journal entries
### New Features (v17.0.1.0.0) ### New Features (v17.0.1.0.0)
#### 1. Total Selected Amount Widget #### 1. Total Selected Amount Widget
A dynamic header widget that displays real-time totals when you select multiple bank statement lines in the list view. A dynamic header widget that displays real-time totals when you select multiple bank statement lines in the list view.
**Features:** **Features:**
- **Real-time Calculation**: Automatically calculates the sum of selected lines - **Real-time Calculation**: Automatically calculates the sum of selected lines
- **Visual Indicators**: - **Visual Indicators**:
- Green badge for positive amounts - Green badge for positive amounts
- Red badge for negative amounts - Red badge for negative amounts
- **Selection Counter**: Shows how many lines are currently selected - **Selection Counter**: Shows how many lines are currently selected
- **Currency Display**: Shows the currency of selected lines - **Currency Display**: Shows the currency of selected lines
- **Modern UI**: Clean, Bootstrap-styled interface with icons - **Modern UI**: Clean, Bootstrap-styled interface with icons
**How to Use:** **How to Use:**
1. Navigate to: `Accounting > Bank Statement Reconciliation > Bank Statement Lines` 1. Navigate to: `Bank Statement Reconciliation`
2. Select one or more bank statement lines by clicking the checkboxes 2. Select one or more bank statement lines by clicking the checkboxes
3. The header widget will automatically appear showing: 3. The footer will showing:
- Number of selected lines - Total amount of selected lines
- Total amount of selected lines
- Currency information ## Installation
**Visual Layout:** 1. Copy the module to your Odoo addons directory:
``` ```bash
┌─────────────────────────────────────────────────────────────┐ cp -r bank_statement_reconciliation /path/to/odoo/addons/
│ ☑ Selected Lines: [3] │ 🧮 Total Amount: [1,234.56] USD │ ```
└─────────────────────────────────────────────────────────────┘
``` 2. Update the addons list:
- Go to `Apps` menu
## Installation - Click `Update Apps List`
- Search for "Bank Statement Reconciliation"
1. Copy the module to your Odoo addons directory:
```bash 3. Install the module:
cp -r bank_statement_reconciliation /path/to/odoo/addons/ - Click `Install` button
```
## Configuration
2. Update the addons list:
- Go to `Apps` menu No additional configuration is required. The module works out of the box after installation.
- Click `Update Apps List`
- Search for "Bank Statement Reconciliation" ## Usage
3. Install the module: ### Reconciling Bank Statement Lines
- Click `Install` button
1. **Access Bank Statement Lines**
## Configuration - Navigate to: `Accounting > Bank Statement Reconciliation > Bank Statement Lines`
No additional configuration is required. The module works out of the box after installation. 2. **Filter Lines**
- Use the search bar to filter by:
## Usage - Date
- Partner
### Reconciling Bank Statement Lines - Journal
- Amount (Income/Expense)
1. **Access Bank Statement Lines**
- Navigate to: `Accounting > Bank Statement Reconciliation > Bank Statement Lines` 3. **Select Lines for Reconciliation**
- Check the boxes next to the lines you want to reconcile
2. **Filter Lines** - The total amount widget will show the sum automatically
- Use the search bar to filter by:
- Date 4. **Initiate Reconciliation**
- Partner - Click the `Action` menu
- Journal - Select `Reconcile Selected Lines`
- Amount (Income/Expense) - OR click the `Reconcile` button in the form view
3. **Select Lines for Reconciliation** 5. **Match Journal Entries**
- Check the boxes next to the lines you want to reconcile - The wizard will open showing available journal entries
- The total amount widget will show the sum automatically - Select the appropriate journal entry to match
- Confirm the reconciliation
4. **Initiate Reconciliation**
- Click the `Action` menu ## Technical Details
- Select `Reconcile Selected Lines`
- OR click the `Reconcile` button in the form view ### Module Structure
5. **Match Journal Entries** ```
- The wizard will open showing available journal entries bank_statement_reconciliation/
- Select the appropriate journal entry to match ├── __init__.py
- Confirm the reconciliation ├── __manifest__.py
├── README.md
## Technical Details ├── models/
│ ├── __init__.py
### Module Structure │ ├── account_bank_statement_line.py
│ ├── bank_statement_line.py
``` │ └── bank_statement_selector.py
bank_statement_reconciliation/ ├── views/
├── __init__.py │ ├── bank_statement_line_views.xml
├── __manifest__.py │ ├── bank_statement_selector_views.xml
├── README.md │ └── menu.xml
├── models/ ├── wizards/
│ ├── __init__.py │ ├── __init__.py
│ ├── account_bank_statement_line.py │ ├── bank_reconcile_wizard.py
│ ├── bank_statement_line.py │ └── bank_reconcile_wizard_views.xml
│ └── bank_statement_selector.py ├── security/
├── views/ │ └── ir.model.access.csv
│ ├── bank_statement_line_views.xml └── static/
│ ├── bank_statement_selector_views.xml └── src/
│ └── menu.xml ├── js/
├── wizards/ │ ├── bank_statement_total_widget.js
│ ├── __init__.py │ ├── bank_statement_list_controller.js
│ ├── bank_reconcile_wizard.py │ └── bank_statement_list_view.js
│ └── bank_reconcile_wizard_views.xml └── xml/
├── security/ ├── bank_statement_total_widget.xml
│ └── ir.model.access.csv └── bank_statement_list_view.xml
└── static/ ```
└── src/
├── js/ ### JavaScript Components
│ ├── bank_statement_total_widget.js
│ ├── bank_statement_list_controller.js #### BankStatementTotalWidget
│ └── bank_statement_list_view.js - **Type**: OWL Component
└── xml/ - **Purpose**: Displays total amount of selected lines
├── bank_statement_total_widget.xml - **Props**:
└── bank_statement_list_view.xml - `resIds`: Array of selected record IDs
``` - **Features**:
- Reactive state management
### JavaScript Components - Automatic updates on selection change
- Currency formatting
#### BankStatementTotalWidget - Visual styling based on amount (positive/negative)
- **Type**: OWL Component
- **Purpose**: Displays total amount of selected lines #### BankStatementListController
- **Props**: - **Type**: List Controller Extension
- `resIds`: Array of selected record IDs - **Purpose**: Extends standard list controller with custom functionality
- **Features**: - **Features**:
- Reactive state management - Provides selected records to the widget
- Automatic updates on selection change - Integrates widget into list view layout
- Currency formatting
- Visual styling based on amount (positive/negative) ### XML Views
#### BankStatementListController #### Tree View Enhancement
- **Type**: List Controller Extension - Added `js_class="bank_statement_list"` attribute
- **Purpose**: Extends standard list controller with custom functionality - Enabled multi-edit mode
- **Features**: - Added monetary widget for amount field
- Provides selected records to the widget - Added currency field for proper formatting
- Integrates widget into list view layout - Includes sum totals in footer
### XML Views ### Dependencies
#### Tree View Enhancement - `account`: Core accounting module
- Added `js_class="bank_statement_list"` attribute - `base`: Odoo base module
- Enabled multi-edit mode - `web`: Web framework for JavaScript components
- Added monetary widget for amount field
- Added currency field for proper formatting ## Customization
- Includes sum totals in footer
### Modifying the Widget Appearance
### Dependencies
Edit [`bank_statement_total_widget.xml`](static/src/xml/bank_statement_total_widget.xml) to customize:
- `account`: Core accounting module - Layout
- `base`: Odoo base module - Colors
- `web`: Web framework for JavaScript components - Icons
- Styling
## Customization
### Extending Functionality
### Modifying the Widget Appearance
To add more calculations or features:
Edit [`bank_statement_total_widget.xml`](static/src/xml/bank_statement_total_widget.xml) to customize:
- Layout 1. Edit [`bank_statement_total_widget.js`](static/src/js/bank_statement_total_widget.js)
- Colors 2. Add new computed properties or methods
- Icons 3. Update the template to display new information
- Styling
Example - Add average calculation:
### Extending Functionality ```javascript
get averageAmount() {
To add more calculations or features: if (this.state.selectedCount === 0) return 0;
return this.state.totalAmount / this.state.selectedCount;
1. Edit [`bank_statement_total_widget.js`](static/src/js/bank_statement_total_widget.js) }
2. Add new computed properties or methods ```
3. Update the template to display new information
## Troubleshooting
Example - Add average calculation:
```javascript ### Widget Not Appearing
get averageAmount() {
if (this.state.selectedCount === 0) return 0; 1. **Clear Browser Cache**
return this.state.totalAmount / this.state.selectedCount; - Hard refresh: `Ctrl+Shift+R` (Windows/Linux) or `Cmd+Shift+R` (Mac)
}
``` 2. **Update Assets**
- Navigate to: `Settings > Technical > User Interface > Views`
## Troubleshooting - Click `Regenerate Assets Bundles`
### Widget Not Appearing 3. **Check JavaScript Console**
- Press `F12` to open developer tools
1. **Clear Browser Cache** - Look for any JavaScript errors
- Hard refresh: `Ctrl+Shift+R` (Windows/Linux) or `Cmd+Shift+R` (Mac)
### Incorrect Totals
2. **Update Assets**
- Navigate to: `Settings > Technical > User Interface > Views` 1. **Verify Currency Settings**
- Click `Regenerate Assets Bundles` - Ensure all selected lines use the same currency
- Check currency rounding settings
3. **Check JavaScript Console**
- Press `F12` to open developer tools 2. **Check Data Integrity**
- Look for any JavaScript errors - Verify amount fields are populated correctly
- Check for null or undefined values
### Incorrect Totals
## Support
1. **Verify Currency Settings**
- Ensure all selected lines use the same currency For issues, questions, or contributions:
- Check currency rounding settings - **Author**: Suherdy Yacob
- **Version**: 17.0.1.0.0
2. **Check Data Integrity** - **Odoo Version**: 17.0
- Verify amount fields are populated correctly
- Check for null or undefined values ## Changelog
## Support ### Version 17.0.1.0.0
- Added total selected amount widget in list view header
For issues, questions, or contributions: - Enhanced tree view with monetary formatting
- **Author**: Suherdy Yacob - Improved user experience with visual indicators
- **Version**: 17.0.1.0.0 - Added real-time calculation of selected lines
- **Odoo Version**: 17.0 - Implemented modern OWL-based JavaScript components
## Changelog ### Initial Version
- Basic bank statement reconciliation functionality
### Version 17.0.1.0.0 - Wizard-based reconciliation process
- Added total selected amount widget in list view header - Multi-line selection support
- Enhanced tree view with monetary formatting - Journal entry matching
- Improved user experience with visual indicators
- Added real-time calculation of selected lines ## License
- Implemented modern OWL-based JavaScript components
This module follows the same license as Odoo.
### Initial Version
- Basic bank statement reconciliation functionality ## Screenshots
- Wizard-based reconciliation process
- Multi-line selection support ### Total Amount Widget in Action
- Journal entry matching When you select multiple bank statement lines, the widget appears at the top showing:
- Number of selected items
## License - Total sum with currency
- Color-coded amount (green for positive, red for negative)
This module follows the same license as Odoo.
### List View Enhancements
## Screenshots - Checkboxes for multi-selection
- Amount column with monetary formatting
### Total Amount Widget in Action - Footer totals for all visible records
When you select multiple bank statement lines, the widget appears at the top showing: - Clean, modern interface
- Number of selected items
- Total sum with currency ## Future Enhancements
- Color-coded amount (green for positive, red for negative)
Planned features for future releases:
### List View Enhancements - [ ] Support for multi-currency reconciliation
- Checkboxes for multi-selection - [ ] Advanced filtering options
- Amount column with monetary formatting - [ ] Reconciliation history tracking
- Footer totals for all visible records - [ ] Export selected lines to Excel
- Clean, modern interface - [ ] Batch operations for common reconciliation patterns
## Future Enhancements
Planned features for future releases:
- [ ] Support for multi-currency reconciliation
- [ ] Advanced filtering options
- [ ] Reconciliation history tracking
- [ ] Export selected lines to Excel
- [ ] Batch operations for common reconciliation patterns
- [ ] AI-powered suggestion for matching entries - [ ] AI-powered suggestion for matching entries

View File

@ -1,2 +1,2 @@
from . import models from . import models
from . import wizards from . import wizards

View File

@ -1,32 +1,32 @@
{ {
'name': 'Bank Statement Reconciliation', 'name': 'Bank Statement Reconciliation',
'version': '17.0.1.0.0', 'version': '17.0.1.0.0',
'category': 'Accounting', 'category': 'Accounting',
'summary': 'Reconcile bank statement lines with journal entries', 'summary': 'Reconcile bank statement lines with journal entries',
'description': """ 'description': """
This module allows users to reconcile bank statement lines with journal entries. This module allows users to reconcile bank statement lines with journal entries.
Features: Features:
- Menu to access bank statement lines - Menu to access bank statement lines
- Filter by bank journal - Filter by bank journal
- Select multiple bank lines to reconcile - Select multiple bank lines to reconcile
- Wizard to select journal entries for reconciliation - Wizard to select journal entries for reconciliation
- Automatic creation of reconciliation journal entries - Automatic creation of reconciliation journal entries
- Total selected amount widget in list view header - Total selected amount widget in list view header
""", """,
'author': 'Suherdy Yacob', 'author': 'Suherdy Yacob',
'depends': [ 'depends': [
'account', 'account',
'base', 'base',
'web', 'web',
], ],
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'views/bank_statement_line_views.xml', 'views/bank_statement_line_views.xml',
'views/bank_statement_selector_views.xml', 'views/bank_statement_selector_views.xml',
'wizards/bank_reconcile_wizard_views.xml', 'wizards/bank_reconcile_wizard_views.xml',
'views/menu.xml', 'views/menu.xml',
], ],
# Tree view includes sum="Total Amount" footer for displaying totals # Tree view includes sum="Total Amount" footer for displaying totals
'installable': True, 'installable': True,
'auto_install': False, 'auto_install': False,
} }

Binary file not shown.

View File

@ -1,3 +1,3 @@
from . import bank_statement_line from . import bank_statement_line
from . import bank_statement_selector from . import bank_statement_selector
from . import account_bank_statement_line from . import account_bank_statement_line

Binary file not shown.

Binary file not shown.

View File

@ -1,35 +1,35 @@
from odoo import models, fields, api from odoo import models, fields, api
from odoo.exceptions import UserError from odoo.exceptions import UserError
class AccountBankStatementLine(models.Model): class AccountBankStatementLine(models.Model):
_inherit = 'account.bank.statement.line' _inherit = 'account.bank.statement.line'
def action_reconcile_selected_lines(self): def action_reconcile_selected_lines(self):
"""Open the reconciliation wizard for selected lines""" """Open the reconciliation wizard for selected lines"""
# Get the selected records from the context # Get the selected records from the context
active_ids = self.env.context.get('active_ids') active_ids = self.env.context.get('active_ids')
active_model = self.env.context.get('active_model') active_model = self.env.context.get('active_model')
if active_model == 'account.bank.statement.line' and active_ids: if active_model == 'account.bank.statement.line' and active_ids:
selected_lines = self.browse(active_ids) selected_lines = self.browse(active_ids)
else: else:
# If called from a single record, use self # If called from a single record, use self
selected_lines = self selected_lines = self
# Filter out already reconciled lines by checking if they have a move_id with reconciliation in the name # 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) unreconciled_lines = selected_lines.filtered(lambda line: not line.move_id)
if not unreconciled_lines: if not unreconciled_lines:
raise UserError("All selected bank statement lines have already been reconciled.") raise UserError("All selected bank statement lines have already been reconciled.")
return { return {
'name': 'Select Journal Entry to Reconcile', 'name': 'Select Journal Entry to Reconcile',
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'res_model': 'bank.reconcile.wizard', 'res_model': 'bank.reconcile.wizard',
'view_mode': 'form', 'view_mode': 'form',
'target': 'new', 'target': 'new',
'context': { 'context': {
'default_bank_line_ids': unreconciled_lines.ids, 'default_bank_line_ids': unreconciled_lines.ids,
} }
} }

View File

@ -1,53 +1,53 @@
from odoo import models, fields, api from odoo import models, fields, api
from odoo.exceptions import UserError from odoo.exceptions import UserError
class BankStatementLine(models.Model): class BankStatementLine(models.Model):
_name = 'bank.statement.line' _name = 'bank.statement.line'
_description = 'Bank Statement Line Selection' _description = 'Bank Statement Line Selection'
@api.model @api.model
def default_get(self, fields): def default_get(self, fields):
res = super().default_get(fields) res = super().default_get(fields)
active_model = self.env.context.get('active_model') active_model = self.env.context.get('active_model')
active_ids = self.env.context.get('active_ids') active_ids = self.env.context.get('active_ids')
if active_model == 'account.bank.statement.line' and active_ids: if active_model == 'account.bank.statement.line' and active_ids:
statement_lines = self.env['account.bank.statement.line'].browse(active_ids) statement_lines = self.env['account.bank.statement.line'].browse(active_ids)
res['line_ids'] = [(6, 0, statement_lines.ids)] res['line_ids'] = [(6, 0, statement_lines.ids)]
return res return res
journal_id = fields.Many2one('account.journal', string='Bank Journal', journal_id = fields.Many2one('account.journal', string='Bank Journal',
domain=[('type', '=', 'bank')]) domain=[('type', '=', 'bank')])
line_ids = fields.Many2many('account.bank.statement.line', string='Bank Statement Lines') line_ids = fields.Many2many('account.bank.statement.line', string='Bank Statement Lines')
selected_line_ids = fields.Many2many('account.bank.statement.line', selected_line_ids = fields.Many2many('account.bank.statement.line',
'bank_statement_line_rel', 'bank_statement_line_rel',
'wizard_id', 'line_id', 'wizard_id', 'line_id',
string='Selected Bank Lines') string='Selected Bank Lines')
@api.onchange('journal_id') @api.onchange('journal_id')
def _onchange_journal_id(self): def _onchange_journal_id(self):
if self.journal_id: if self.journal_id:
statement_lines = self.env['account.bank.statement.line'].search([ statement_lines = self.env['account.bank.statement.line'].search([
('journal_id', '=', self.journal_id.id) ('journal_id', '=', self.journal_id.id)
]) ])
self.line_ids = statement_lines self.line_ids = statement_lines
else: else:
self.line_ids = False self.line_ids = False
def action_open_reconcile_wizard(self): def action_open_reconcile_wizard(self):
"""Open the reconciliation wizard""" """Open the reconciliation wizard"""
if not self.selected_line_ids: if not self.selected_line_ids:
raise UserError("Please select at least one bank statement line to reconcile.") raise UserError("Please select at least one bank statement line to reconcile.")
return { return {
'name': 'Select Journal Entry to Reconcile', 'name': 'Select Journal Entry to Reconcile',
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'res_model': 'bank.reconcile.wizard', 'res_model': 'bank.reconcile.wizard',
'view_mode': 'form', 'view_mode': 'form',
'target': 'new', 'target': 'new',
'context': { 'context': {
'default_bank_line_ids': self.selected_line_ids.ids, 'default_bank_line_ids': self.selected_line_ids.ids,
} }
} }

View File

@ -1,29 +1,29 @@
from odoo import models, fields, api from odoo import models, fields, api
class BankStatementSelector(models.TransientModel): class BankStatementSelector(models.TransientModel):
_name = 'bank.statement.selector' _name = 'bank.statement.selector'
_description = 'Bank Statement Selector' _description = 'Bank Statement Selector'
journal_id = fields.Many2one('account.journal', journal_id = fields.Many2one('account.journal',
string='Bank Journal', string='Bank Journal',
domain=[('type', '=', 'bank')], domain=[('type', '=', 'bank')],
required=True) required=True)
def action_show_statement_lines(self): def action_show_statement_lines(self):
"""Open the bank statement lines for the selected journal""" """Open the bank statement lines for the selected journal"""
action = { action = {
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'name': 'Bank Statement Lines', 'name': 'Bank Statement Lines',
'res_model': 'account.bank.statement.line', 'res_model': 'account.bank.statement.line',
'view_mode': 'tree,form', 'view_mode': 'tree,form',
'domain': [('journal_id', '=', self.journal_id.id)], 'domain': [('journal_id', '=', self.journal_id.id)],
'context': { 'context': {
'search_default_journal_id': self.journal_id.id, 'search_default_journal_id': self.journal_id.id,
}, },
'views': [ '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_tree').id, 'tree'),
(self.env.ref('bank_statement_reconciliation.view_account_bank_statement_line_form').id, 'form') (self.env.ref('bank_statement_reconciliation.view_account_bank_statement_line_form').id, 'form')
] ]
} }
return action return action

View File

@ -1,4 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 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_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_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 access_bank_statement_selector,access_bank_statement_selector,model_bank_statement_selector,account.group_account_user,1,1,1,1
1 id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2 access_bank_statement_line,access_bank_statement_line,model_bank_statement_line,account.group_account_user,1,1,1,1
3 access_bank_reconcile_wizard,access_bank_reconcile_wizard,model_bank_reconcile_wizard,account.group_account_user,1,1
4 access_bank_statement_selector,access_bank_statement_selector,model_bank_statement_selector,account.group_account_user,1,1,1,1

View File

@ -1,29 +1,29 @@
/** @odoo-module **/ /** @odoo-module **/
import { ListController } from "@web/views/list/list_controller"; import { ListController } from "@web/views/list/list_controller";
import { BankStatementTotalWidget } from "./bank_statement_total_widget"; import { BankStatementTotalWidget } from "./bank_statement_total_widget";
import { patch } from "@web/core/utils/patch"; import { patch } from "@web/core/utils/patch";
patch(ListController.prototype, { patch(ListController.prototype, {
get selectedRecords() { get selectedRecords() {
if (this.props.resModel === "account.bank.statement.line") { if (this.props.resModel === "account.bank.statement.line") {
return this.model.root.selection.map(record => record.resId); return this.model.root.selection.map(record => record.resId);
} }
return []; return [];
} }
}); });
export class BankStatementListController extends ListController { export class BankStatementListController extends ListController {
setup() { setup() {
super.setup(); super.setup();
} }
get selectedRecords() { get selectedRecords() {
return this.model.root.selection.map(record => record.resId); return this.model.root.selection.map(record => record.resId);
} }
} }
BankStatementListController.components = { BankStatementListController.components = {
...ListController.components, ...ListController.components,
BankStatementTotalWidget, BankStatementTotalWidget,
}; };

View File

@ -1,12 +1,12 @@
/** @odoo-module **/ /** @odoo-module **/
import { registry } from "@web/core/registry"; import { registry } from "@web/core/registry";
import { listView } from "@web/views/list/list_view"; import { listView } from "@web/views/list/list_view";
import { BankStatementListController } from "./bank_statement_list_controller"; import { BankStatementListController } from "./bank_statement_list_controller";
export const bankStatementListView = { export const bankStatementListView = {
...listView, ...listView,
Controller: BankStatementListController, Controller: BankStatementListController,
}; };
registry.category("views").add("bank_statement_list", bankStatementListView); registry.category("views").add("bank_statement_list", bankStatementListView);

View File

@ -1,81 +1,81 @@
/** @odoo-module **/ /** @odoo-module **/
import { Component, useState, onWillStart, onWillUpdateProps } from "@odoo/owl"; import { Component, useState, onWillStart, onWillUpdateProps } from "@odoo/owl";
import { registry } from "@web/core/registry"; import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks"; import { useService } from "@web/core/utils/hooks";
export class BankStatementTotalWidget extends Component { export class BankStatementTotalWidget extends Component {
setup() { setup() {
this.orm = useService("orm"); this.orm = useService("orm");
this.state = useState({ this.state = useState({
totalAmount: 0, totalAmount: 0,
selectedCount: 0, selectedCount: 0,
currency: null, currency: null,
}); });
onWillStart(async () => { onWillStart(async () => {
await this.updateTotals(); await this.updateTotals();
}); });
onWillUpdateProps(async (nextProps) => { onWillUpdateProps(async (nextProps) => {
await this.updateTotals(nextProps); await this.updateTotals(nextProps);
}); });
} }
async updateTotals(props = this.props) { async updateTotals(props = this.props) {
const resIds = props.resIds || []; const resIds = props.resIds || [];
if (resIds.length === 0) { if (resIds.length === 0) {
this.state.totalAmount = 0; this.state.totalAmount = 0;
this.state.selectedCount = 0; this.state.selectedCount = 0;
this.state.currency = null; this.state.currency = null;
return; return;
} }
try { try {
const records = await this.orm.searchRead( const records = await this.orm.searchRead(
"account.bank.statement.line", "account.bank.statement.line",
[["id", "in", resIds]], [["id", "in", resIds]],
["amount", "currency_id"] ["amount", "currency_id"]
); );
let total = 0; let total = 0;
let currency = null; let currency = null;
records.forEach(record => { records.forEach(record => {
total += record.amount || 0; total += record.amount || 0;
if (!currency && record.currency_id) { if (!currency && record.currency_id) {
currency = record.currency_id; currency = record.currency_id;
} }
}); });
this.state.totalAmount = total; this.state.totalAmount = total;
this.state.selectedCount = records.length; this.state.selectedCount = records.length;
this.state.currency = currency; this.state.currency = currency;
} catch (error) { } catch (error) {
console.error("Error fetching bank statement totals:", error); console.error("Error fetching bank statement totals:", error);
this.state.totalAmount = 0; this.state.totalAmount = 0;
this.state.selectedCount = 0; this.state.selectedCount = 0;
this.state.currency = null; this.state.currency = null;
} }
} }
get formattedTotal() { get formattedTotal() {
if (!this.state.currency) { if (!this.state.currency) {
return this.state.totalAmount.toFixed(2); return this.state.totalAmount.toFixed(2);
} }
// Format with currency symbol if available // Format with currency symbol if available
return this.state.totalAmount.toFixed(2); return this.state.totalAmount.toFixed(2);
} }
get displayClass() { get displayClass() {
return this.state.totalAmount < 0 ? 'text-danger' : 'text-success'; return this.state.totalAmount < 0 ? 'text-danger' : 'text-success';
} }
} }
BankStatementTotalWidget.template = "bank_statement_reconciliation.BankStatementTotalWidget"; BankStatementTotalWidget.template = "bank_statement_reconciliation.BankStatementTotalWidget";
BankStatementTotalWidget.props = { BankStatementTotalWidget.props = {
resIds: { type: Array, optional: true }, resIds: { type: Array, optional: true },
}; };
registry.category("view_widgets").add("bank_statement_total_widget", BankStatementTotalWidget); registry.category("view_widgets").add("bank_statement_total_widget", BankStatementTotalWidget);

View File

@ -1,32 +1,32 @@
/** @odoo-module **/ /** @odoo-module **/
import { patch } from "@web/core/utils/patch"; import { patch } from "@web/core/utils/patch";
import { ListController } from "@web/views/list/list_controller"; import { ListController } from "@web/views/list/list_controller";
patch(ListController.prototype, { patch(ListController.prototype, {
/** /**
* Compute total amount of selected bank statement lines * Compute total amount of selected bank statement lines
*/ */
get selectedBankLinesTotal() { get selectedBankLinesTotal() {
if (this.props.resModel !== "account.bank.statement.line") { if (this.props.resModel !== "account.bank.statement.line") {
return null; return null;
} }
const selection = this.model.root.selection; const selection = this.model.root.selection;
if (!selection || selection.length === 0) { if (!selection || selection.length === 0) {
return null; return null;
} }
let total = 0; let total = 0;
selection.forEach(record => { selection.forEach(record => {
const amount = record.data.amount || 0; const amount = record.data.amount || 0;
total += amount; total += amount;
}); });
return { return {
count: selection.length, count: selection.length,
total: total.toFixed(2), total: total.toFixed(2),
currency: selection[0]?.data.currency_id?.[1] || 'IDR' currency: selection[0]?.data.currency_id?.[1] || 'IDR'
}; };
} }
}); });

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve"> <templates xml:space="preserve">
<t t-name="bank_statement_reconciliation.BankStatementListController" t-inherit="web.ListView" t-inherit-mode="extension"> <t t-name="bank_statement_reconciliation.BankStatementListController" t-inherit="web.ListView" t-inherit-mode="extension">
<xpath expr="//div[hasclass('o_content')]" position="before"> <xpath expr="//div[hasclass('o_content')]" position="before">
<div class="o_bank_statement_header_widget p-3 mx-3" t-if="props.resModel === 'account.bank.statement.line' and selectedRecords.length > 0"> <div class="o_bank_statement_header_widget p-3 mx-3" t-if="props.resModel === 'account.bank.statement.line' and selectedRecords.length > 0">
<BankStatementTotalWidget resIds="selectedRecords"/> <BankStatementTotalWidget resIds="selectedRecords"/>
</div> </div>
</xpath> </xpath>
</t> </t>
</templates> </templates>

View File

@ -1,15 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve"> <templates xml:space="preserve">
<t t-name="bank_statement_reconciliation.SelectionInfo" t-inherit="web.ListView" t-inherit-mode="extension"> <t t-name="bank_statement_reconciliation.SelectionInfo" t-inherit="web.ListView" t-inherit-mode="extension">
<xpath expr="//Layout" position="inside"> <xpath expr="//Layout" position="inside">
<t t-set-slot="layout-actions"> <t t-set-slot="layout-actions">
<div t-if="model.root.selection.length > 0" class="alert alert-info d-flex align-items-center gap-3 m-2" role="alert"> <div t-if="model.root.selection.length > 0" class="alert alert-info d-flex align-items-center gap-3 m-2" role="alert">
<i class="fa fa-info-circle fa-lg"/> <i class="fa fa-info-circle fa-lg"/>
<strong t-esc="model.root.selection.length"/> lines selected <strong t-esc="model.root.selection.length"/> lines selected
<span class="mx-2">|</span> <span class="mx-2">|</span>
Total: <strong t-esc="model.root.selection.reduce((sum, rec) => sum + (rec.data.amount || 0), 0).toFixed(2)"/> Total: <strong t-esc="model.root.selection.reduce((sum, rec) => sum + (rec.data.amount || 0), 0).toFixed(2)"/>
</div> </div>
</t> </t>
</xpath> </xpath>
</t> </t>
</templates> </templates>

View File

@ -1,19 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve"> <templates xml:space="preserve">
<t t-name="bank_statement_reconciliation.BankStatementTotalWidget"> <t t-name="bank_statement_reconciliation.BankStatementTotalWidget">
<div class="o_bank_statement_total_widget d-flex align-items-center gap-3 px-3 py-2 bg-light border rounded"> <div class="o_bank_statement_total_widget d-flex align-items-center gap-3 px-3 py-2 bg-light border rounded">
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<i class="fa fa-check-square-o text-primary" style="font-size: 1.2em;"/> <i class="fa fa-check-square-o text-primary" style="font-size: 1.2em;"/>
<span class="fw-bold">Selected Lines:</span> <span class="fw-bold">Selected Lines:</span>
<span class="badge bg-primary" t-esc="state.selectedCount"/> <span class="badge bg-primary" t-esc="state.selectedCount"/>
</div> </div>
<div class="vr"/> <div class="vr"/>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<i class="fa fa-calculator text-info" style="font-size: 1.2em;"/> <i class="fa fa-calculator text-info" style="font-size: 1.2em;"/>
<span class="fw-bold">Total Amount:</span> <span class="fw-bold">Total Amount:</span>
<span t-attf-class="badge {{displayClass}} fs-6 px-3" t-esc="formattedTotal"/> <span t-attf-class="badge {{displayClass}} fs-6 px-3" t-esc="formattedTotal"/>
<span t-if="state.currency" class="text-muted small" t-esc="state.currency[1]"/> <span t-if="state.currency" class="text-muted small" t-esc="state.currency[1]"/>
</div> </div>
</div> </div>
</t> </t>
</templates> </templates>

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve"> <templates xml:space="preserve">
<t t-name="bank_statement_reconciliation.SelectedAmountBadge" t-inherit="web.ListController" t-inherit-mode="extension"> <t t-name="bank_statement_reconciliation.SelectedAmountBadge" t-inherit="web.ListController" t-inherit-mode="extension">
<xpath expr="//div[hasclass('o_cp_action_menus')]" position="before"> <xpath expr="//div[hasclass('o_cp_action_menus')]" position="before">
<div t-if="selectedBankLinesTotal" class="badge bg-info text-dark fs-5 px-3 py-2 me-2"> <div t-if="selectedBankLinesTotal" class="badge bg-info text-dark fs-5 px-3 py-2 me-2">
<i class="fa fa-calculator me-1"/> <i class="fa fa-calculator me-1"/>
Total: Rp <t t-esc="selectedBankLinesTotal.total"/> Total: Rp <t t-esc="selectedBankLinesTotal.total"/>
</div> </div>
</xpath> </xpath>
</t> </t>
</templates> </templates>

View File

@ -1,109 +1,109 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<!-- Action to open the bank statement lines view --> <!-- Action to open the bank statement lines view -->
<record id="action_bank_statement_lines" model="ir.actions.act_window"> <record id="action_bank_statement_lines" model="ir.actions.act_window">
<field name="name">Bank Statement Lines</field> <field name="name">Bank Statement Lines</field>
<field name="res_model">account.bank.statement.line</field> <field name="res_model">account.bank.statement.line</field>
<field name="view_mode">tree,form</field> <field name="view_mode">tree,form</field>
<field name="context">{'tree_view_ref': 'bank_statement_reconciliation.view_account_bank_statement_line_tree'}</field> <field name="context">{'tree_view_ref': 'bank_statement_reconciliation.view_account_bank_statement_line_tree'}</field>
<field name="help" type="html"> <field name="help" type="html">
<p class="o_view_nocontent_smiling_face"> <p class="o_view_nocontent_smiling_face">
Select a bank journal to view its statement lines Select a bank journal to view its statement lines
</p> </p>
</field> </field>
</record> </record>
<!-- Server action for reconciliation --> <!-- Server action for reconciliation -->
<record id="action_reconcile_bank_lines" model="ir.actions.server"> <record id="action_reconcile_bank_lines" model="ir.actions.server">
<field name="name">Reconcile Selected Lines</field> <field name="name">Reconcile Selected Lines</field>
<field name="model_id" ref="account.model_account_bank_statement_line"/> <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="binding_model_id" ref="account.model_account_bank_statement_line"/>
<field name="state">code</field> <field name="state">code</field>
<field name="code"> <field name="code">
action = { action = {
'name': 'Select Journal Entry to Reconcile', 'name': 'Select Journal Entry to Reconcile',
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'res_model': 'bank.reconcile.wizard', 'res_model': 'bank.reconcile.wizard',
'view_mode': 'form', 'view_mode': 'form',
'target': 'new', 'target': 'new',
'context': { 'context': {
'default_bank_line_ids': env.context.get('active_ids'), 'default_bank_line_ids': env.context.get('active_ids'),
} }
} }
</field> </field>
</record> </record>
<!-- Tree view for bank statement lines --> <!-- Tree view for bank statement lines -->
<record id="view_account_bank_statement_line_tree" model="ir.ui.view"> <record id="view_account_bank_statement_line_tree" model="ir.ui.view">
<field name="name">account.bank.statement.line.tree</field> <field name="name">account.bank.statement.line.tree</field>
<field name="model">account.bank.statement.line</field> <field name="model">account.bank.statement.line</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Bank Statement Lines" create="0" delete="0" decoration-danger="amount &lt; 0" decoration-muted="move_id and 'Reconciliation:' in move_id.name" multi_edit="1"> <tree string="Bank Statement Lines" create="0" delete="0" decoration-danger="amount &lt; 0" decoration-muted="move_id and 'Reconciliation:' in move_id.name" multi_edit="1">
<field name="company_id" column_invisible="True"/> <field name="company_id" column_invisible="True"/>
<field name="currency_id" column_invisible="True"/> <field name="currency_id" column_invisible="True"/>
<field name="suitable_journal_ids" column_invisible="True"/> <field name="suitable_journal_ids" column_invisible="True"/>
<field name="date"/> <field name="date"/>
<field name="name"/> <field name="name"/>
<field name="partner_id"/> <field name="partner_id"/>
<field name="amount" sum="Total Amount" widget="monetary"/> <field name="amount" sum="Total Amount" widget="monetary"/>
<field name="journal_id"/> <field name="journal_id"/>
<field name="statement_id"/> <field name="statement_id"/>
<field name="move_id"/> <field name="move_id"/>
</tree> </tree>
</field> </field>
</record> </record>
<!-- Form view for bank statement lines --> <!-- Form view for bank statement lines -->
<record id="view_account_bank_statement_line_form" model="ir.ui.view"> <record id="view_account_bank_statement_line_form" model="ir.ui.view">
<field name="name">account.bank.statement.line.form</field> <field name="name">account.bank.statement.line.form</field>
<field name="model">account.bank.statement.line</field> <field name="model">account.bank.statement.line</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Bank Statement Line"> <form string="Bank Statement Line">
<header> <header>
<button name="action_reconcile_selected_lines" type="object" string="Reconcile" class="btn-primary" invisible="move_id and 'Reconciliation:' in move_id.name"/> <button name="action_reconcile_selected_lines" type="object" string="Reconcile" class="btn-primary" invisible="move_id and 'Reconciliation:' in move_id.name"/>
</header> </header>
<sheet> <sheet>
<group> <group>
<group> <group>
<field name="date"/> <field name="date"/>
<field name="name"/> <field name="name"/>
<field name="ref"/> <field name="ref"/>
</group> </group>
<group> <group>
<field name="amount"/> <field name="amount"/>
<field name="partner_id"/> <field name="partner_id"/>
<field name="journal_id" readonly="1"/> <field name="journal_id" readonly="1"/>
</group> </group>
</group> </group>
<group> <group>
<field name="statement_id"/> <field name="statement_id"/>
<field name="move_id"/> <field name="move_id"/>
<field name="company_id" invisible="1"/> <field name="company_id" invisible="1"/>
</group> </group>
</sheet> </sheet>
</form> </form>
</field> </field>
</record> </record>
<!-- Search view for bank statement lines --> <!-- Search view for bank statement lines -->
<record id="view_account_bank_statement_line_search" model="ir.ui.view"> <record id="view_account_bank_statement_line_search" model="ir.ui.view">
<field name="name">account.bank.statement.line.search</field> <field name="name">account.bank.statement.line.search</field>
<field name="model">account.bank.statement.line</field> <field name="model">account.bank.statement.line</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<search> <search>
<field name="name"/> <field name="name"/>
<field name="partner_id"/> <field name="partner_id"/>
<field name="journal_id"/> <field name="journal_id"/>
<field name="date"/> <field name="date"/>
<filter name="positive_amount" string="Income" domain="[('amount', '&gt;', 0)]"/> <filter name="positive_amount" string="Income" domain="[('amount', '&gt;', 0)]"/>
<filter name="negative_amount" string="Expense" domain="[('amount', '&lt;', 0)]"/> <filter name="negative_amount" string="Expense" domain="[('amount', '&lt;', 0)]"/>
<filter name="hide_reconciled" string="Hide Reconciled" domain="[('move_id', 'not ilike', '%Reconciliation:%')]" help="Hide lines that have been reconciled"/> <filter name="hide_reconciled" string="Hide Reconciled" domain="[('move_id', 'not ilike', '%Reconciliation:%')]" help="Hide lines that have been reconciled"/>
<filter name="show_all" string="Show All" domain="[]" help="Show all lines including reconciled"/> <filter name="show_all" string="Show All" domain="[]" help="Show all lines including reconciled"/>
<group expand="0" string="Group By"> <group expand="0" string="Group By">
<filter name="group_by_journal" string="Journal" context="{'group_by': 'journal_id'}"/> <filter name="group_by_journal" string="Journal" context="{'group_by': 'journal_id'}"/>
<filter name="group_by_date" string="Date" context="{'group_by': 'date'}"/> <filter name="group_by_date" string="Date" context="{'group_by': 'date'}"/>
</group> </group>
</search> </search>
</field> </field>
</record> </record>
</odoo> </odoo>

View File

@ -1,29 +1,29 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<!-- Form view for bank statement selector --> <!-- Form view for bank statement selector -->
<record id="view_bank_statement_selector_form" model="ir.ui.view"> <record id="view_bank_statement_selector_form" model="ir.ui.view">
<field name="name">bank.statement.selector.form</field> <field name="name">bank.statement.selector.form</field>
<field name="model">bank.statement.selector</field> <field name="model">bank.statement.selector</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Select Bank Journal"> <form string="Select Bank Journal">
<group> <group>
<field name="journal_id" options="{'no_create': True}"/> <field name="journal_id" options="{'no_create': True}"/>
</group> </group>
<footer> <footer>
<button name="action_show_statement_lines" type="object" string="Show Statement Lines" <button name="action_show_statement_lines" type="object" string="Show Statement Lines"
class="btn-primary"/> class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/> <button string="Cancel" class="btn-secondary" special="cancel"/>
</footer> </footer>
</form> </form>
</field> </field>
</record> </record>
<!-- Action to open the bank statement selector --> <!-- Action to open the bank statement selector -->
<record id="action_bank_statement_selector" model="ir.actions.act_window"> <record id="action_bank_statement_selector" model="ir.actions.act_window">
<field name="name">Select Bank Journal</field> <field name="name">Select Bank Journal</field>
<field name="res_model">bank.statement.selector</field> <field name="res_model">bank.statement.selector</field>
<field name="view_mode">form</field> <field name="view_mode">form</field>
<field name="target">new</field> <field name="target">new</field>
<field name="view_id" ref="view_bank_statement_selector_form"/> <field name="view_id" ref="view_bank_statement_selector_form"/>
</record> </record>
</odoo> </odoo>

View File

@ -1,15 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<!-- Top level menu for Bank Statement Reconciliation --> <!-- Top level menu for Bank Statement Reconciliation -->
<menuitem id="menu_bank_statement_root" <menuitem id="menu_bank_statement_root"
name="Bank Statement Reconciliation" name="Bank Statement Reconciliation"
sequence="50" sequence="50"
web_icon="account,static/description/icon.png" web_icon="account,static/description/icon.png"
groups="account.group_account_user"/> groups="account.group_account_user"/>
<menuitem id="menu_bank_statement_lines" <menuitem id="menu_bank_statement_lines"
name="Bank Statement Lines" name="Bank Statement Lines"
parent="menu_bank_statement_root" parent="menu_bank_statement_root"
action="action_bank_statement_selector" action="action_bank_statement_selector"
sequence="10"/> sequence="10"/>
</odoo> </odoo>

Binary file not shown.

View File

@ -1,93 +1,93 @@
from odoo import models, fields, api from odoo import models, fields, api
from odoo.exceptions import UserError from odoo.exceptions import UserError
class BankReconcileWizard(models.TransientModel): class BankReconcileWizard(models.TransientModel):
_name = 'bank.reconcile.wizard' _name = 'bank.reconcile.wizard'
_description = 'Bank Reconcile Wizard' _description = 'Bank Reconcile Wizard'
bank_line_ids = fields.Many2many('account.bank.statement.line', bank_line_ids = fields.Many2many('account.bank.statement.line',
string='Selected Bank Lines', string='Selected Bank Lines',
readonly=True) readonly=True)
journal_entry_id = fields.Many2one('account.move', journal_entry_id = fields.Many2one('account.move',
string='Journal Entry to Reconcile', string='Journal Entry to Reconcile',
domain=[('state', '=', 'posted')]) domain=[('state', '=', 'posted')])
journal_entry_line_id = fields.Many2one('account.move.line', journal_entry_line_id = fields.Many2one('account.move.line',
string='Journal Entry Line to Reconcile', string='Journal Entry Line to Reconcile',
domain="[('move_id', '=', journal_entry_id), ('reconciled', '=', False)]") 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) total_bank_line_amount = fields.Float(string='Total Bank Line Amount', compute='_compute_total_bank_line_amount', store=True)
@api.onchange('journal_entry_id') @api.onchange('journal_entry_id')
def _onchange_journal_entry_id(self): def _onchange_journal_entry_id(self):
"""Reset journal entry line when journal entry changes""" """Reset journal entry line when journal entry changes"""
if self.journal_entry_id: if self.journal_entry_id:
self.journal_entry_line_id = False self.journal_entry_line_id = False
else: else:
self.journal_entry_line_id = False self.journal_entry_line_id = False
@api.depends('bank_line_ids') @api.depends('bank_line_ids')
def _compute_total_bank_line_amount(self): def _compute_total_bank_line_amount(self):
"""Compute total amount of selected bank lines""" """Compute total amount of selected bank lines"""
for record in self: for record in self:
record.total_bank_line_amount = sum(line.amount for line in record.bank_line_ids) record.total_bank_line_amount = sum(line.amount for line in record.bank_line_ids)
def action_reconcile(self): def action_reconcile(self):
"""Perform the reconciliation for each selected bank line""" """Perform the reconciliation for each selected bank line"""
if not self.journal_entry_id: if not self.journal_entry_id:
raise UserError("Please select a journal entry to reconcile.") raise UserError("Please select a journal entry to reconcile.")
if not self.journal_entry_line_id: if not self.journal_entry_line_id:
raise UserError("Please select a journal entry line to reconcile.") raise UserError("Please select a journal entry line to reconcile.")
# Check if any of the selected bank lines are already reconciled # Check if any of the selected bank lines are already reconciled
for bank_line in self.bank_line_ids: for bank_line in self.bank_line_ids:
if bank_line.move_id and bank_line.move_id.ref and 'Reconciliation:' in bank_line.move_id.ref: if bank_line.move_id and bank_line.move_id.ref and 'Reconciliation:' in bank_line.move_id.ref:
raise UserError(f"Bank statement line '{bank_line.ref}' has already been reconciled and cannot be reconciled again.") raise UserError(f"Bank statement line '{bank_line.ref}' has already been reconciled and cannot be reconciled again.")
# Process each selected bank line individually # Process each selected bank line individually
for bank_line in self.bank_line_ids: for bank_line in self.bank_line_ids:
self._reconcile_single_line(bank_line, self.journal_entry_line_id) self._reconcile_single_line(bank_line, self.journal_entry_line_id)
return {'type': 'ir.actions.act_window_close'} return {'type': 'ir.actions.act_window_close'}
def _reconcile_single_line(self, bank_line, journal_entry_line): def _reconcile_single_line(self, bank_line, journal_entry_line):
"""Reconcile a single bank line with a journal entry line""" """Reconcile a single bank line with a journal entry line"""
# Create a journal entry to balance the transaction # Create a journal entry to balance the transaction
# This mimics the standard Odoo reconciliation widget behavior. # This mimics the standard Odoo reconciliation widget behavior.
move = self.env['account.move'].create({ move = self.env['account.move'].create({
'journal_id': bank_line.journal_id.id, 'journal_id': bank_line.journal_id.id,
'date': bank_line.date, 'date': bank_line.date,
'ref': f'Reconciliation: {bank_line.name or "Bank Line"}', 'ref': f'Reconciliation: {bank_line.name or "Bank Line"}',
'move_type': 'entry', 'move_type': 'entry',
'line_ids': [ 'line_ids': [
(0, 0, { (0, 0, {
'account_id': bank_line.journal_id.default_account_id.id, 'account_id': bank_line.journal_id.default_account_id.id,
'debit': bank_line.amount if bank_line.amount > 0 else 0, 'debit': bank_line.amount if bank_line.amount > 0 else 0,
'credit': -bank_line.amount if bank_line.amount < 0 else 0, 'credit': -bank_line.amount if bank_line.amount < 0 else 0,
'name': f'Bank Reconciliation: {bank_line.name or ""}', 'name': f'Bank Reconciliation: {bank_line.name or ""}',
}), }),
(0, 0, { (0, 0, {
'account_id': journal_entry_line.account_id.id, 'account_id': journal_entry_line.account_id.id,
'debit': -bank_line.amount if bank_line.amount < 0 else 0, 'debit': -bank_line.amount if bank_line.amount < 0 else 0,
'credit': bank_line.amount if bank_line.amount > 0 else 0, 'credit': bank_line.amount if bank_line.amount > 0 else 0,
'name': f'Bank Reconciliation: {journal_entry_line.name or ""}', 'name': f'Bank Reconciliation: {journal_entry_line.name or ""}',
}), }),
], ],
}) })
move.action_post() move.action_post()
# Link the bank statement line to the new journal entry # Link the bank statement line to the new journal entry
bank_line.write({ bank_line.write({
'move_id': move.id, 'move_id': move.id,
}) })
# Find the corresponding line in the new move and reconcile with the journal entry line # Find the corresponding line in the new move and reconcile with the journal entry line
move_line = move.line_ids.filtered(lambda l: l.account_id.id == journal_entry_line.account_id.id) move_line = move.line_ids.filtered(lambda l: l.account_id.id == journal_entry_line.account_id.id)
if move_line: if move_line:
try: try:
(journal_entry_line + move_line).reconcile() (journal_entry_line + move_line).reconcile()
except: except:
pass pass

View File

@ -1,36 +1,36 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<!-- Wizard form view --> <!-- Wizard form view -->
<record id="view_bank_reconcile_wizard_form" model="ir.ui.view"> <record id="view_bank_reconcile_wizard_form" model="ir.ui.view">
<field name="name">bank.reconcile.wizard.form</field> <field name="name">bank.reconcile.wizard.form</field>
<field name="model">bank.reconcile.wizard</field> <field name="model">bank.reconcile.wizard</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Reconcile Bank Lines"> <form string="Reconcile Bank Lines">
<group> <group>
<field name="bank_line_ids" widget="many2many" options="{'no_create': True}" readonly="1"/> <field name="bank_line_ids" widget="many2many" options="{'no_create': True}" readonly="1"/>
</group> </group>
<group> <group>
<field name="total_bank_line_amount" readonly="1"/> <field name="total_bank_line_amount" readonly="1"/>
<field name="journal_entry_id" options="{'no_create': True}"/> <field name="journal_entry_id" options="{'no_create': True}"/>
<field name="journal_entry_line_id" <field name="journal_entry_line_id"
domain="[('move_id', '=', journal_entry_id), ('reconciled', '=', False)]" domain="[('move_id', '=', journal_entry_id), ('reconciled', '=', False)]"
invisible="journal_entry_id == False"/> invisible="journal_entry_id == False"/>
</group> </group>
<footer> <footer>
<button name="action_reconcile" type="object" string="Reconcile" <button name="action_reconcile" type="object" string="Reconcile"
class="btn-primary" class="btn-primary"
invisible="journal_entry_id == False or journal_entry_line_id == False"/> invisible="journal_entry_id == False or journal_entry_line_id == False"/>
<button string="Cancel" class="btn-secondary" special="cancel"/> <button string="Cancel" class="btn-secondary" special="cancel"/>
</footer> </footer>
</form> </form>
</field> </field>
</record> </record>
<!-- Action to open the wizard --> <!-- Action to open the wizard -->
<record id="action_bank_reconcile_wizard" model="ir.actions.act_window"> <record id="action_bank_reconcile_wizard" model="ir.actions.act_window">
<field name="name">Select Journal Entry to Reconcile</field> <field name="name">Select Journal Entry to Reconcile</field>
<field name="res_model">bank.reconcile.wizard</field> <field name="res_model">bank.reconcile.wizard</field>
<field name="view_mode">form</field> <field name="view_mode">form</field>
<field name="target">new</field> <field name="target">new</field>
</record> </record>
</odoo> </odoo>