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

View File

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

View File

@ -1,32 +1,32 @@
{
'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
- Total selected amount widget in list view header
""",
'author': 'Suherdy Yacob',
'depends': [
'account',
'base',
'web',
],
'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',
],
# Tree view includes sum="Total Amount" footer for displaying totals
'installable': True,
'auto_install': False,
{
'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
- Total selected amount widget in list view header
""",
'author': 'Suherdy Yacob',
'depends': [
'account',
'base',
'web',
],
'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',
],
# Tree view includes sum="Total Amount" footer for displaying totals
'installable': True,
'auto_install': False,
}

Binary file not shown.

View File

@ -1,3 +1,3 @@
from . import bank_statement_line
from . import bank_statement_selector
from . import bank_statement_line
from . import bank_statement_selector
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.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)
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,
}
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)
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,
}
}

View File

@ -1,53 +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,
}
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,
}
}

View File

@ -1,29 +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')
]
}
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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="bank_statement_reconciliation.BankStatementListController" t-inherit="web.ListView" t-inherit-mode="extension">
<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">
<BankStatementTotalWidget resIds="selectedRecords"/>
</div>
</xpath>
</t>
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="bank_statement_reconciliation.BankStatementListController" t-inherit="web.ListView" t-inherit-mode="extension">
<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">
<BankStatementTotalWidget resIds="selectedRecords"/>
</div>
</xpath>
</t>
</templates>

View File

@ -1,15 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="bank_statement_reconciliation.SelectionInfo" t-inherit="web.ListView" t-inherit-mode="extension">
<xpath expr="//Layout" position="inside">
<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">
<i class="fa fa-info-circle fa-lg"/>
<strong t-esc="model.root.selection.length"/> lines selected
<span class="mx-2">|</span>
Total: <strong t-esc="model.root.selection.reduce((sum, rec) => sum + (rec.data.amount || 0), 0).toFixed(2)"/>
</div>
</t>
</xpath>
</t>
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="bank_statement_reconciliation.SelectionInfo" t-inherit="web.ListView" t-inherit-mode="extension">
<xpath expr="//Layout" position="inside">
<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">
<i class="fa fa-info-circle fa-lg"/>
<strong t-esc="model.root.selection.length"/> lines selected
<span class="mx-2">|</span>
Total: <strong t-esc="model.root.selection.reduce((sum, rec) => sum + (rec.data.amount || 0), 0).toFixed(2)"/>
</div>
</t>
</xpath>
</t>
</templates>

View File

@ -1,19 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<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="d-flex align-items-center gap-2">
<i class="fa fa-check-square-o text-primary" style="font-size: 1.2em;"/>
<span class="fw-bold">Selected Lines:</span>
<span class="badge bg-primary" t-esc="state.selectedCount"/>
</div>
<div class="vr"/>
<div class="d-flex align-items-center gap-2">
<i class="fa fa-calculator text-info" style="font-size: 1.2em;"/>
<span class="fw-bold">Total Amount:</span>
<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]"/>
</div>
</div>
</t>
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<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="d-flex align-items-center gap-2">
<i class="fa fa-check-square-o text-primary" style="font-size: 1.2em;"/>
<span class="fw-bold">Selected Lines:</span>
<span class="badge bg-primary" t-esc="state.selectedCount"/>
</div>
<div class="vr"/>
<div class="d-flex align-items-center gap-2">
<i class="fa fa-calculator text-info" style="font-size: 1.2em;"/>
<span class="fw-bold">Total Amount:</span>
<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]"/>
</div>
</div>
</t>
</templates>

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<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">
<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"/>
Total: Rp <t t-esc="selectedBankLinesTotal.total"/>
</div>
</xpath>
</t>
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<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">
<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"/>
Total: Rp <t t-esc="selectedBankLinesTotal.total"/>
</div>
</xpath>
</t>
</templates>

View File

@ -1,109 +1,109 @@
<?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="context">{'tree_view_ref': 'bank_statement_reconciliation.view_account_bank_statement_line_tree'}</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 &lt; 0" decoration-muted="move_id and 'Reconciliation:' in move_id.name" multi_edit="1">
<field name="company_id" column_invisible="True"/>
<field name="currency_id" column_invisible="True"/>
<field name="suitable_journal_ids" column_invisible="True"/>
<field name="date"/>
<field name="name"/>
<field name="partner_id"/>
<field name="amount" sum="Total Amount" widget="monetary"/>
<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', '&gt;', 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="show_all" string="Show All" domain="[]" help="Show all lines including reconciled"/>
<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>
<?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="context">{'tree_view_ref': 'bank_statement_reconciliation.view_account_bank_statement_line_tree'}</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 &lt; 0" decoration-muted="move_id and 'Reconciliation:' in move_id.name" multi_edit="1">
<field name="company_id" column_invisible="True"/>
<field name="currency_id" column_invisible="True"/>
<field name="suitable_journal_ids" column_invisible="True"/>
<field name="date"/>
<field name="name"/>
<field name="partner_id"/>
<field name="amount" sum="Total Amount" widget="monetary"/>
<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', '&gt;', 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="show_all" string="Show All" domain="[]" help="Show all lines including reconciled"/>
<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>

View File

@ -1,29 +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>
<?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>

View File

@ -1,15 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Top level menu for Bank Statement Reconciliation -->
<menuitem id="menu_bank_statement_root"
name="Bank Statement Reconciliation"
sequence="50"
web_icon="account,static/description/icon.png"
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"/>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Top level menu for Bank Statement Reconciliation -->
<menuitem id="menu_bank_statement_root"
name="Bank Statement Reconciliation"
sequence="50"
web_icon="account,static/description/icon.png"
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>

Binary file not shown.

View File

@ -1,93 +1,93 @@
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.")
# Check if any of the selected bank lines are already reconciled
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:
raise UserError(f"Bank statement line '{bank_line.ref}' has already been reconciled and cannot be reconciled again.")
# 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"""
# Create a journal entry to balance the transaction
# This mimics the standard Odoo reconciliation widget behavior.
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': bank_line.journal_id.default_account_id.id,
'debit': 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 ""}',
}),
(0, 0, {
'account_id': journal_entry_line.account_id.id,
'debit': -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 ""}',
}),
],
})
move.action_post()
# Link the bank statement line to the new journal entry
bank_line.write({
'move_id': move.id,
})
# 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)
if move_line:
try:
(journal_entry_line + move_line).reconcile()
except:
pass
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.")
# Check if any of the selected bank lines are already reconciled
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:
raise UserError(f"Bank statement line '{bank_line.ref}' has already been reconciled and cannot be reconciled again.")
# 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"""
# Create a journal entry to balance the transaction
# This mimics the standard Odoo reconciliation widget behavior.
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': bank_line.journal_id.default_account_id.id,
'debit': 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 ""}',
}),
(0, 0, {
'account_id': journal_entry_line.account_id.id,
'debit': -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 ""}',
}),
],
})
move.action_post()
# Link the bank statement line to the new journal entry
bank_line.write({
'move_id': move.id,
})
# 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)
if move_line:
try:
(journal_entry_line + move_line).reconcile()
except:
pass

View File

@ -1,36 +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>
<?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>