diff --git a/base_accounting_kit/__manifest__.py b/base_accounting_kit/__manifest__.py
index fa30fa993..95bdaf53d 100644
--- a/base_accounting_kit/__manifest__.py
+++ b/base_accounting_kit/__manifest__.py
@@ -21,7 +21,7 @@
#############################################################################
{
'name': 'Odoo 18 Full Accounting Kit for Community',
- 'version': '18.0.1.0.2',
+ 'version': '18.0.2.0.2',
'category': 'Accounting',
'live_test_url': 'https://kit.easyinstance.com/web/login?redirect=/odoo/accounting',
'summary': """Odoo 18 Accounting, Odoo 18 Accounting Reports, Odoo18 Accounting, Odoo Accounting, Odoo18 Financial Reports, Odoo18 Asset, Odoo18 Profit and Loss, PDC, Followups, Odoo18, Accounting, Odoo Apps, Reports""",
@@ -94,7 +94,11 @@
'views/account_bank_statement_line_views.xml',
'views/account_payment_view.xml',
'wizard/account_lock_date_views.xml',
+ 'wizard/import_bank_statement_views.xml',
],
+ 'external_dependencies': {
+ 'python': ['openpyxl', 'ofxparse']
+ },
'assets': {
'web.assets_backend': [
'base_accounting_kit/static/src/scss/style.scss',
diff --git a/base_accounting_kit/doc/RELEASE_NOTES.md b/base_accounting_kit/doc/RELEASE_NOTES.md
index 1e397bb9b..1f2830164 100644
--- a/base_accounting_kit/doc/RELEASE_NOTES.md
+++ b/base_accounting_kit/doc/RELEASE_NOTES.md
@@ -15,3 +15,7 @@
#### FIX
- Fixed the Asset creation issue from the vendor bill.
+#### 27.03.2025
+#### Version 18.0.2.0.2
+#### UPDT
+- Added the Import Bank Statement feature.
diff --git a/base_accounting_kit/models/account_journal.py b/base_accounting_kit/models/account_journal.py
index 1376f84c1..077bfd6ed 100644
--- a/base_accounting_kit/models/account_journal.py
+++ b/base_accounting_kit/models/account_journal.py
@@ -115,3 +115,15 @@ class AccountJournal(models.Model):
'view_mode': 'list,form',
'context': {'default_journal_id': self.id},
}
+
+ def action_import_wizard(self):
+ """Function to open wizard"""
+ return {
+ 'type': 'ir.actions.act_window',
+ 'view_mode': 'form',
+ 'res_model': 'import.bank.statement',
+ 'target': 'new',
+ 'context': {
+ 'default_journal_id': self.id,
+ }
+ }
diff --git a/base_accounting_kit/security/ir.model.access.csv b/base_accounting_kit/security/ir.model.access.csv
index b89efcea1..fa4383332 100644
--- a/base_accounting_kit/security/ir.model.access.csv
+++ b/base_accounting_kit/security/ir.model.access.csv
@@ -45,3 +45,5 @@ access_account_account_type,account.account.type,model_account_account_type,acco
access_account_lock_date,access.account.lock.date,model_account_lock_date,account.group_account_user,1,1,1,1
access_account_recurring_entries_line,access.account.recurring.entries.line,model_account_recurring_entries_line,account.group_account_user,1,1,1,1
access_generate_recurring_entries,generate.recurring.entries.user,model_account_recurring_payments,account.group_account_user,1,1,1,1
+
+access_import_bank_statement_user,access.import.bank.statement.user,model_import_bank_statement,base.group_user,1,1,1,1
diff --git a/base_accounting_kit/static/description/assets/screenshots/bank_statements.png b/base_accounting_kit/static/description/assets/screenshots/bank_statements.png
new file mode 100644
index 000000000..516db83cc
Binary files /dev/null and b/base_accounting_kit/static/description/assets/screenshots/bank_statements.png differ
diff --git a/base_accounting_kit/static/description/assets/screenshots/import_button.png b/base_accounting_kit/static/description/assets/screenshots/import_button.png
new file mode 100644
index 000000000..12529f80f
Binary files /dev/null and b/base_accounting_kit/static/description/assets/screenshots/import_button.png differ
diff --git a/base_accounting_kit/static/description/assets/screenshots/import_wizard.png b/base_accounting_kit/static/description/assets/screenshots/import_wizard.png
new file mode 100644
index 000000000..5609c94d6
Binary files /dev/null and b/base_accounting_kit/static/description/assets/screenshots/import_wizard.png differ
diff --git a/base_accounting_kit/static/description/assets/screenshots/statement.csv b/base_accounting_kit/static/description/assets/screenshots/statement.csv
new file mode 100644
index 000000000..04ca33a35
--- /dev/null
+++ b/base_accounting_kit/static/description/assets/screenshots/statement.csv
@@ -0,0 +1,3 @@
+Account name,Amount,Amount in Currency,Date,Partner,Running Balance
+Bank-2023-04-14/1,2000,,2023-04-14,Deco Addict,10044.87
+Bank-2023-04-21/2,1000,,2023-03-20,Ready Mat,10044.8
diff --git a/base_accounting_kit/static/description/assets/screenshots/statement.ofx b/base_accounting_kit/static/description/assets/screenshots/statement.ofx
new file mode 100644
index 000000000..d246b13e5
--- /dev/null
+++ b/base_accounting_kit/static/description/assets/screenshots/statement.ofx
@@ -0,0 +1,65 @@
+OFXHEADER:100
+DATA:OFXSGML
+VERSION:102
+SECURITY:NONE
+ENCODING:USASCII
+CHARSET:1252
+COMPRESSION:NONE
+OLDFILEUID:NONE
+NEWFILEUID:NONE
+
+
+
+
+
+ 0
+ INFO
+
+ 20230419
+ ENG
+
+ BANK123
+
+
+
+
+
+ 1234567890
+
+ 0
+ INFO
+
+
+ USD
+
+ Bank-2023-04-14/1
+ 1234567890
+ CHECKING
+
+
+ 20220101
+ 20220131
+
+ DEBIT
+ 20230419
+ -3000.00
+ 1234567890-20220102-1
+ Deco Addict
+
+
+ CREDIT
+ 20220115
+ 1000.00
+ 1234567890-20220115-1
+ Ready Mat
+
+
+
+ 10044.87
+ 20220131
+
+
+
+
+
+
diff --git a/base_accounting_kit/static/description/assets/screenshots/statement.qif b/base_accounting_kit/static/description/assets/screenshots/statement.qif
new file mode 100644
index 000000000..837d2fc4f
--- /dev/null
+++ b/base_accounting_kit/static/description/assets/screenshots/statement.qif
@@ -0,0 +1,15 @@
+!Type:Bank
+D14/04/2023
+T2000.00
+PBank-2023-02-11/1
+^
+!Type:Bank
+D09/04/2023
+T-500.00
+PBank-2023-04-09/2
+^
+!Type:Bank
+D19/06/2022
+T1000.00
+PBank-2023-04-21/3
+^
diff --git a/base_accounting_kit/static/description/assets/screenshots/statement.xlsx b/base_accounting_kit/static/description/assets/screenshots/statement.xlsx
new file mode 100644
index 000000000..56af415c8
Binary files /dev/null and b/base_accounting_kit/static/description/assets/screenshots/statement.xlsx differ
diff --git a/base_accounting_kit/static/description/index.html b/base_accounting_kit/static/description/index.html
index c3f1faeeb..de245b231 100644
--- a/base_accounting_kit/static/description/index.html
+++ b/base_accounting_kit/static/description/index.html
@@ -530,6 +530,90 @@
+
+
+
+

+
+
+ Bank Statement CSV File Format.
+
+
+ We can import Bank Statement in CSV file.
+
+
+
+
+
+
+

+
+
+ Bank Statement XLSX File Format.
+
+
+ We can import Bank Statement in XLSX File Format.
+
+
+
+
+
+
+

+
+
+ Bank Statement OFX File Format.
+
+
+ We can import Bank Statement in OFX File Format.
+
+
+
+
+
+
+

+
+
+ Bank Statement QIF File Format.
+
+
+ We can import Bank Statement in QIF File Format.
+
+
+
@@ -1302,6 +1386,89 @@
+
+
+
+
+
+
+ A quick option to import bank statement.
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+ A wizard that allows user to upload file.
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+ After importing the file user can view the statements that imported
+
+
+
+
+
+
+ After importing the file user can view the statements that imported. (The example format for csv, xlsx, ofx and qif are added in screenshots folder in the module.).
+
+
+
+
+
+

+
+
+
+
+
to Reconcile
+
+ Import
+
+
diff --git a/base_accounting_kit/wizard/__init__.py b/base_accounting_kit/wizard/__init__.py
index be8068123..7a9b9133e 100644
--- a/base_accounting_kit/wizard/__init__.py
+++ b/base_accounting_kit/wizard/__init__.py
@@ -33,4 +33,5 @@ from . import asset_depreciation_confirmation
from . import asset_modify
from . import cash_flow_report
from . import financial_report
+from . import import_bank_statement
from . import kit_account_tax_report
diff --git a/base_accounting_kit/wizard/import_bank_statement.py b/base_accounting_kit/wizard/import_bank_statement.py
new file mode 100644
index 000000000..9ececcd57
--- /dev/null
+++ b/base_accounting_kit/wizard/import_bank_statement.py
@@ -0,0 +1,336 @@
+# -*- coding: utf-8 -*-
+###############################################################################
+#
+# Cybrosys Technologies Pvt. Ltd.
+#
+# Copyright (C) 2024-TODAY Cybrosys Technologies()
+# Author: Akhil Ashok (odoo@cybrosys.com)
+#
+# You can modify it under the terms of the GNU LESSER
+# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
+#
+# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
+# (LGPL v3) along with this program.
+# If not, see .
+#
+###############################################################################
+import base64
+import codecs
+import openpyxl
+import os
+from datetime import datetime
+from io import BytesIO
+from odoo import fields, models, _
+from odoo.exceptions import ValidationError
+from ofxparse import OfxParser
+from qifparse.parser import QifParser
+
+
+class ImportBankStatement(models.TransientModel):
+ """ A class to import files as bank statement """
+ _name = "import.bank.statement"
+ _description = "Import button"
+ _rec_name = "file_name"
+
+ attachment = fields.Binary(string="File", required=True,
+ help="Choose the file to import")
+ file_name = fields.Char(string="File Name", help="Name of the file")
+ journal_id = fields.Many2one('account.journal', string="Journal ID",
+ help="Journal in which the file importing")
+
+ def action_statement_import(self):
+ """Function to import csv, xlsx, ofx and qif file format"""
+ split_tup = os.path.splitext(self.file_name)
+ if split_tup[1] == '.csv' or split_tup[1] == '.xlsx' or split_tup[
+ 1] == '.ofx' or split_tup[1] == '.qif':
+ if split_tup[1] == '.csv':
+ # Reading csv file
+ try:
+ file = base64.b64decode(self.attachment)
+ file_string = file.decode('utf-8')
+ file_string = file_string.split('\n')
+ except:
+ raise ValidationError(_("Choose correct file"))
+ # Skipping the first line
+ firstline = True
+ for file_item in file_string:
+ if firstline:
+ firstline = False
+ continue
+ # Reading the content from csv file
+ if file_item.split(',') != ['']:
+ if file_item.split(',')[0] and file_item.split(',')[1] \
+ and file_item.split(',')[4]:
+ date_obj = str(fields.date.today()) if not \
+ file_item.split(',')[3] else \
+ file_item.split(',')[
+ 3]
+ transaction_date = datetime.strptime(date_obj,
+ "%Y-%m-%d")
+ partner = self.env['res.partner'].search(
+ [('name', '=', file_item.split(',')[4])])
+ # Creating a record in account.bank.statement model
+ if partner:
+ statement = self.env[
+ 'account.bank.statement'].create({
+ 'name': file_item.split(',')[0],
+ 'line_ids': [
+ (0, 0, {
+ 'date': transaction_date,
+ 'payment_ref': 'csv file',
+ 'partner_id': partner.id,
+ 'journal_id': self.journal_id.id,
+ 'amount': file_item.split(',')[1],
+ 'amount_currency':
+ file_item.split(',')[2],
+ }),
+ ],
+ })
+ else:
+ raise ValidationError(_("Partner not exist"))
+ else:
+ if not file_item.split(',')[0]:
+ raise ValidationError(
+ _("Account name is not set"))
+ elif not file_item.split(',')[1]:
+ raise ValidationError(
+ _("Amount is not set"))
+ elif not file_item.split(',')[4]:
+ date_obj = str(fields.date.today()) if not \
+ file_item.split(',')[3] else \
+ file_item.split(',')[
+ 3]
+ transaction_date = datetime.strptime(date_obj,
+ "%Y-%m-%d")
+ # Creating a record in account.bank.statement model
+ statement = self.env[
+ 'account.bank.statement'].create({
+ 'name': file_item.split(',')[0],
+ 'line_ids': [
+ (0, 0, {
+ 'date': transaction_date,
+ 'payment_ref': 'csv file',
+ 'journal_id': self.journal_id.id,
+ 'amount': file_item.split(',')[
+ 1],
+ 'amount_currency':
+ file_item.split(',')[2],
+ }),
+ ],
+ })
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Statements',
+ 'view_mode': 'list',
+ 'res_model': 'account.bank.statement',
+ 'res_id': statement.id,
+ }
+ elif split_tup[1] == '.xlsx':
+ # Reading xlsx file
+ try:
+ order = openpyxl.load_workbook(
+ filename=BytesIO(base64.b64decode(self.attachment)))
+ xl_order = order.active
+ except:
+ raise ValidationError(_("Choose correct file"))
+ for record in xl_order.iter_rows(min_row=2, max_row=None,
+ min_col=None,
+ max_col=None,
+ values_only=True):
+ line = list(record)
+ # Reading the content from file
+ if line[0] and line[1] and line[3]:
+ partner = self.env['res.partner'].search(
+ [('name', '=', line[3])])
+ date_obj = fields.date.today() if not line[2] else \
+ line[2].date()
+ # Creating record
+ if partner:
+ statement = self.env[
+ 'account.bank.statement'].create({
+ 'name': line[0],
+ 'line_ids': [
+ (0, 0, {
+ 'date': date_obj,
+ 'payment_ref': 'xlsx file',
+ 'partner_id': partner.id,
+ 'journal_id': self.journal_id.id,
+ 'amount': line[1],
+ }),
+ ],
+ })
+ else:
+ raise ValidationError(_("Partner not exist"))
+ else:
+ if not line[0]:
+ raise ValidationError(
+ _("Account name is not set"))
+ elif not line[1]:
+ raise ValidationError(
+ _("Amount is not set"))
+ elif not line[3]:
+ date_obj = fields.date.today() if not line[2] else \
+ line[2].date()
+ # Creating record
+ statement = self.env[
+ 'account.bank.statement'].create({
+ 'name': line[0],
+ 'line_ids': [
+ (0, 0, {
+ 'date': date_obj,
+ 'payment_ref': 'xlsx file',
+ 'journal_id': self.journal_id.id,
+ 'amount': line[1],
+ }),
+ ],
+ })
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Statements',
+ 'view_mode': 'list',
+ 'res_model': 'account.bank.statement',
+ 'res_id': statement.id,
+ }
+ elif split_tup[1] == '.ofx':
+ # Searching the path of the file
+ file_attachment = self.env["ir.attachment"].search(
+ ['|', ('res_field', '!=', False),
+ ('res_field', '=', False),
+ ('res_id', '=', self.id),
+ ('res_model', '=', 'import.bank.statement')],
+ limit=1)
+ file_path = file_attachment._full_path(
+ file_attachment.store_fname)
+ # Parsing the file
+ try:
+ with codecs.open(file_path) as fileobj:
+ ofx_file = OfxParser.parse(fileobj)
+ except:
+ raise ValidationError(_("Wrong file format"))
+ if not ofx_file.account:
+ raise ValidationError(
+ _("No account information found in OFX file."))
+ if not ofx_file.account.statement:
+ raise ValidationError(
+ _("No statement information found in OFX file."))
+ statement_list = []
+ # Reading the content from file
+ for transaction in ofx_file.account.statement.transactions:
+ if transaction.type == "debit" and transaction.amount != 0:
+ payee = transaction.payee
+ amount = transaction.amount
+ date = transaction.date
+ if not date:
+ date = fields.date.today()
+ partner = self.env['res.partner'].search(
+ [('name', '=', payee)])
+ if partner:
+ statement_list.append([partner.id, amount, date])
+ else:
+ raise ValidationError(_("Partner not exist"))
+ if transaction.type == "credit" and transaction.amount != 0:
+ payee = transaction.payee
+ amount = transaction.amount
+ date = transaction.date
+ if not date:
+ date = fields.date.today()
+ partner = self.env['res.partner'].search(
+ [('name', '=', payee)])
+ if partner:
+ statement_list.append([partner.id, amount, date])
+ else:
+ raise ValidationError(_("Partner not exist"))
+ # Creating record
+ if statement_list:
+ for item in statement_list:
+ statement = self.env['account.bank.statement'].create({
+ 'name': ofx_file.account.routing_number,
+ 'line_ids': [
+ (0, 0, {
+ 'date': item[2],
+ 'payment_ref': 'ofx file',
+ 'partner_id': item[0],
+ 'journal_id': self.journal_id.id,
+ 'amount': item[1],
+ }),
+ ],
+ })
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Statements',
+ 'view_mode': 'list',
+ 'res_model': 'account.bank.statement',
+ 'res_id': statement.id,
+ }
+ else:
+ raise ValidationError(_("There is no data to import"))
+ elif split_tup[1] == '.qif':
+ # Searching the path of qif file
+ file_attachment = self.env["ir.attachment"].search(
+ ['|', ('res_field', '!=', False),
+ ('res_field', '=', False),
+ ('res_id', '=', self.id),
+ ('res_model', '=', 'import.bank.statement')],
+ limit=1)
+ file_path = file_attachment._full_path(
+ file_attachment.store_fname)
+ # Parsing the qif file
+ try:
+ parser = QifParser()
+ with open(file_path, 'r') as qiffile:
+ qif = parser.parse(qiffile)
+ except:
+ raise ValidationError(_("Wrong file format"))
+ file_string = str(qif)
+ file_item = file_string.split('^')
+ file_item[-1] = file_item[-1].rstrip('\n')
+ if file_item[-1] == '':
+ file_item.pop()
+ statement_list = []
+ for item in file_item:
+ if not item.startswith('!Type:Bank'):
+ item = '!Type:Bank' + item
+ data = item.split('\n')
+ # Reading the file content
+ date_entry = data[1][1:]
+ amount = float(data[2][1:])
+ payee = data[3][1:]
+ if amount and payee:
+ if not date_entry:
+ date_entry = str(fields.date.today())
+ date_object = datetime.strptime(date_entry, '%d/%m/%Y')
+ date = date_object.strftime('%Y-%m-%d')
+ statement_list.append([payee, amount, date])
+ else:
+ if not amount:
+ raise ValidationError(_("Amount is not set"))
+ elif not payee:
+ raise ValidationError(_("Payee is not set"))
+ # Creating record
+ if statement_list:
+ for item in statement_list:
+ statement = self.env['account.bank.statement'].create({
+ 'name': item[0],
+ 'line_ids': [
+ (0, 0, {
+ 'date': item[2],
+ 'payment_ref': 'qif file',
+ 'journal_id': self.journal_id.id,
+ 'amount': item[1],
+ }),
+ ],
+ })
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Statements',
+ 'view_mode': 'list',
+ 'res_model': 'account.bank.statement',
+ 'res_id': statement.id,
+ }
+ else:
+ raise ValidationError(_("Choose correct file"))
diff --git a/base_accounting_kit/wizard/import_bank_statement_views.xml b/base_accounting_kit/wizard/import_bank_statement_views.xml
new file mode 100644
index 000000000..40c33b6ad
--- /dev/null
+++ b/base_accounting_kit/wizard/import_bank_statement_views.xml
@@ -0,0 +1,41 @@
+
+
+
+
+ import.bank.statement.view.form
+ import.bank.statement
+
+
+
+
+
+
+ import.bank.statement.view.action
+ ir.actions.act_window
+ import.bank.statement
+ list,form
+
+ new
+
+