mirror of
https://github.com/OCA/bank-statement-import.git
synced 2025-01-20 12:37:43 +02:00
[ADD] account_bank_statement_import_split: split statements during import
This commit is contained in:
3
account_bank_statement_import_split/__init__.py
Normal file
3
account_bank_statement_import_split/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import models
|
||||
21
account_bank_statement_import_split/__manifest__.py
Normal file
21
account_bank_statement_import_split/__manifest__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Copyright 2019 Brainbean Apps (https://brainbeanapps.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
'name': 'Online Bank Statements Import Split',
|
||||
'version': '12.0.1.0.0',
|
||||
'author':
|
||||
'Brainbean Apps, '
|
||||
'Odoo Community Association (OCA)',
|
||||
'website': 'https://github.com/OCA/bank-statement-import/',
|
||||
'license': 'AGPL-3',
|
||||
'category': 'Accounting',
|
||||
'summary': 'Split statements during import',
|
||||
'depends': [
|
||||
'account_bank_statement_import',
|
||||
],
|
||||
'data': [
|
||||
'views/account_bank_statement_import.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
3
account_bank_statement_import_split/models/__init__.py
Normal file
3
account_bank_statement_import_split/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import account_bank_statement_import
|
||||
@@ -0,0 +1,153 @@
|
||||
# Copyright 2019 Brainbean Apps (https://brainbeanapps.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
from dateutil.relativedelta import relativedelta, MO
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class AccountBankStatementImport(models.TransientModel):
|
||||
_inherit = 'account.bank.statement.import'
|
||||
|
||||
import_mode = fields.Selection(
|
||||
selection=[
|
||||
('single', 'Single statement'),
|
||||
('daily', 'Daily statements'),
|
||||
('weekly', 'Weekly statements'),
|
||||
('monthly', 'Monthly statements'),
|
||||
],
|
||||
default='single',
|
||||
)
|
||||
|
||||
def _complete_stmts_vals(self, stmts_vals, journal, account_number):
|
||||
stmts_vals = super()._complete_stmts_vals(
|
||||
stmts_vals,
|
||||
journal,
|
||||
account_number
|
||||
)
|
||||
if not self.import_mode or self.import_mode == 'single':
|
||||
return stmts_vals
|
||||
statements = []
|
||||
for st_vals in stmts_vals:
|
||||
transactions = list(sorted(
|
||||
map(
|
||||
lambda transaction: self._prepare_transaction(
|
||||
transaction
|
||||
),
|
||||
st_vals['transactions']
|
||||
),
|
||||
key=lambda transaction: transaction['date']
|
||||
))
|
||||
if not transactions:
|
||||
continue
|
||||
del st_vals['transactions']
|
||||
|
||||
balance_start = Decimal(st_vals['balance_start']) \
|
||||
if 'balance_start' in st_vals else None
|
||||
balance_end = Decimal(st_vals['balance_end_real']) \
|
||||
if 'balance_end_real' in st_vals else None
|
||||
statement_date_since = self._get_statement_date_since(
|
||||
transactions[0]['date']
|
||||
)
|
||||
while transactions:
|
||||
statement_date_until = (
|
||||
statement_date_since + self._get_statement_date_step()
|
||||
)
|
||||
|
||||
last_transaction_index = None
|
||||
for index, transaction in enumerate(transactions):
|
||||
if transaction['date'] >= statement_date_until:
|
||||
break
|
||||
last_transaction_index = index
|
||||
if last_transaction_index is None:
|
||||
# NOTE: No transactions for current period
|
||||
statement_date_since = statement_date_until
|
||||
continue
|
||||
|
||||
statement_transactions = \
|
||||
transactions[0:last_transaction_index + 1]
|
||||
transactions = transactions[last_transaction_index + 1:]
|
||||
|
||||
statement_values = dict(st_vals)
|
||||
statement_values.update({
|
||||
'name': self._get_statement_name(
|
||||
journal,
|
||||
statement_date_since,
|
||||
statement_date_until,
|
||||
),
|
||||
'date': self._get_statement_date(
|
||||
statement_date_since,
|
||||
statement_date_until,
|
||||
),
|
||||
'transactions': statement_transactions,
|
||||
})
|
||||
if balance_start is not None:
|
||||
statement_values.update({
|
||||
'balance_start': float(balance_start),
|
||||
})
|
||||
for transaction in statement_transactions:
|
||||
balance_start += Decimal(transaction['amount'])
|
||||
if balance_end is not None:
|
||||
statement_balance_end = balance_end
|
||||
for transaction in transactions:
|
||||
statement_balance_end -= Decimal(transaction['amount'])
|
||||
statement_values.update({
|
||||
'balance_end_real': float(statement_balance_end),
|
||||
})
|
||||
|
||||
statements.append(statement_values)
|
||||
statement_date_since = statement_date_until
|
||||
return statements
|
||||
|
||||
@api.multi
|
||||
def _prepare_transaction(self, transaction):
|
||||
transaction.update({
|
||||
'date': fields.Date.from_string(transaction['date']),
|
||||
})
|
||||
return transaction
|
||||
|
||||
@api.multi
|
||||
def _get_statement_date_since(self, date):
|
||||
self.ensure_one()
|
||||
if self.import_mode == 'daily':
|
||||
return date
|
||||
elif self.import_mode == 'weekly':
|
||||
return date + relativedelta(weekday=MO(-1))
|
||||
elif self.import_mode == 'monthly':
|
||||
return date.replace(
|
||||
day=1,
|
||||
)
|
||||
|
||||
@api.multi
|
||||
def _get_statement_date_step(self):
|
||||
self.ensure_one()
|
||||
if self.import_mode == 'daily':
|
||||
return relativedelta(
|
||||
days=1,
|
||||
)
|
||||
elif self.import_mode == 'weekly':
|
||||
return relativedelta(
|
||||
weeks=1,
|
||||
weekday=MO,
|
||||
)
|
||||
elif self.import_mode == 'monthly':
|
||||
return relativedelta(
|
||||
months=1,
|
||||
day=1,
|
||||
)
|
||||
|
||||
@api.multi
|
||||
def _get_statement_date(self, date_since, date_until):
|
||||
self.ensure_one()
|
||||
# NOTE: Statement date is treated by Odoo as start of period. Details
|
||||
# - addons/account/models/account_journal_dashboard.py
|
||||
# - def get_line_graph_datas()
|
||||
return date_since
|
||||
|
||||
@api.multi
|
||||
def _get_statement_name(self, journal, date_since, date_until):
|
||||
self.ensure_one()
|
||||
return journal.sequence_id.with_context(
|
||||
ir_sequence_date=self._get_statement_date(date_since, date_until)
|
||||
).next_by_id()
|
||||
@@ -0,0 +1 @@
|
||||
* Alexey Pelykh <alexey.pelykh@brainbeanapps.com>
|
||||
@@ -0,0 +1,5 @@
|
||||
This module allows splitting statements by date during import:
|
||||
|
||||
* as daily statements
|
||||
* as weekly statements
|
||||
* as monthly statements
|
||||
3
account_bank_statement_import_split/tests/__init__.py
Normal file
3
account_bank_statement_import_split/tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import test_account_bank_statement_import_split
|
||||
@@ -0,0 +1,267 @@
|
||||
# Copyright 2019 Brainbean Apps (https://brainbeanapps.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields
|
||||
from odoo.tests import common
|
||||
|
||||
from base64 import b64encode
|
||||
from unittest import mock
|
||||
|
||||
_parse_file_method = (
|
||||
'odoo.addons.account_bank_statement_import'
|
||||
'.account_bank_statement_import.AccountBankStatementImport._parse_file'
|
||||
)
|
||||
|
||||
|
||||
class TestAccountBankAccountStatementImportSplit(common.TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.now = fields.Datetime.now()
|
||||
self.currency_usd = self.env.ref('base.USD')
|
||||
self.empty_data_file = b64encode(
|
||||
'TestAccountBankAccountStatementImportSplit'.encode('utf-8')
|
||||
)
|
||||
self.AccountJournal = self.env['account.journal']
|
||||
self.AccountBankStatement = self.env['account.bank.statement']
|
||||
self.AccountBankStatementImport = self.env[
|
||||
'account.bank.statement.import'
|
||||
]
|
||||
|
||||
def test_default_import_mode(self):
|
||||
journal = self.AccountJournal.create({
|
||||
'name': 'Bank',
|
||||
'type': 'bank',
|
||||
'code': 'BANK',
|
||||
'currency_id': self.currency_usd.id,
|
||||
})
|
||||
wizard = self.AccountBankStatementImport.with_context({
|
||||
'journal_id': journal.id,
|
||||
}).create({
|
||||
'filename': 'file.ext',
|
||||
'data_file': self.empty_data_file,
|
||||
})
|
||||
data = (
|
||||
journal.currency_id.name,
|
||||
journal.bank_account_id.acc_number,
|
||||
[{
|
||||
'name': 'STATEMENT',
|
||||
'date': '2019-01-01',
|
||||
'balance_start': 0.0,
|
||||
'balance_end_real': 100.0,
|
||||
'transactions': [{
|
||||
'name': 'TRANSACTION',
|
||||
'amount': '100.0',
|
||||
'date': '2019-01-01',
|
||||
'note': 'NOTE',
|
||||
'unique_import_id': 'TRANSACTION-ID',
|
||||
}],
|
||||
}],
|
||||
)
|
||||
with mock.patch(_parse_file_method, return_value=data):
|
||||
wizard.with_context({
|
||||
'journal_id': journal.id,
|
||||
}).import_file()
|
||||
statement = self.AccountBankStatement.search([
|
||||
('journal_id', '=', journal.id),
|
||||
])
|
||||
self.assertEqual(len(statement), 1)
|
||||
self.assertEqual(len(statement.line_ids), 1)
|
||||
|
||||
def test_single_import_mode(self):
|
||||
journal = self.AccountJournal.create({
|
||||
'name': 'Bank',
|
||||
'type': 'bank',
|
||||
'code': 'BANK',
|
||||
'currency_id': self.currency_usd.id,
|
||||
})
|
||||
wizard = self.AccountBankStatementImport.with_context({
|
||||
'journal_id': journal.id,
|
||||
}).create({
|
||||
'filename': 'file.ext',
|
||||
'data_file': self.empty_data_file,
|
||||
'import_mode': 'single',
|
||||
})
|
||||
data = (
|
||||
journal.currency_id.name,
|
||||
journal.bank_account_id.acc_number,
|
||||
[{
|
||||
'name': 'STATEMENT',
|
||||
'date': '2019-01-01',
|
||||
'balance_start': 0.0,
|
||||
'balance_end_real': 100.0,
|
||||
'transactions': [{
|
||||
'name': 'TRANSACTION',
|
||||
'amount': '100.0',
|
||||
'date': '2019-01-01',
|
||||
'note': 'NOTE',
|
||||
'unique_import_id': 'TRANSACTION-ID',
|
||||
}],
|
||||
}],
|
||||
)
|
||||
with mock.patch(_parse_file_method, return_value=data):
|
||||
wizard.with_context({
|
||||
'journal_id': journal.id,
|
||||
}).import_file()
|
||||
statement = self.AccountBankStatement.search([
|
||||
('journal_id', '=', journal.id),
|
||||
])
|
||||
self.assertEqual(len(statement), 1)
|
||||
self.assertEqual(len(statement.line_ids), 1)
|
||||
|
||||
def test_daily_import_mode(self):
|
||||
journal = self.AccountJournal.create({
|
||||
'name': 'Bank',
|
||||
'type': 'bank',
|
||||
'code': 'BANK',
|
||||
'currency_id': self.currency_usd.id,
|
||||
})
|
||||
wizard = self.AccountBankStatementImport.with_context({
|
||||
'journal_id': journal.id,
|
||||
}).create({
|
||||
'filename': 'file.ext',
|
||||
'data_file': self.empty_data_file,
|
||||
'import_mode': 'daily',
|
||||
})
|
||||
data = (
|
||||
journal.currency_id.name,
|
||||
journal.bank_account_id.acc_number,
|
||||
[{
|
||||
'name': 'STATEMENT',
|
||||
'date': '2019-01-01',
|
||||
'balance_start': 0.0,
|
||||
'balance_end_real': 100.0,
|
||||
'transactions': [{
|
||||
'name': 'TRANSACTION-1',
|
||||
'amount': '50.0',
|
||||
'date': '2019-01-01',
|
||||
'note': 'NOTE',
|
||||
'unique_import_id': 'TRANSACTION-ID-1',
|
||||
}, {
|
||||
'name': 'TRANSACTION-2',
|
||||
'amount': '50.0',
|
||||
'date': '2019-01-03',
|
||||
'note': 'NOTE',
|
||||
'unique_import_id': 'TRANSACTION-ID-2',
|
||||
}],
|
||||
}],
|
||||
)
|
||||
with mock.patch(_parse_file_method, return_value=data):
|
||||
wizard.with_context({
|
||||
'journal_id': journal.id,
|
||||
}).import_file()
|
||||
statements = self.AccountBankStatement.search([
|
||||
('journal_id', '=', journal.id),
|
||||
]).sorted(key=lambda statement: statement.date)
|
||||
self.assertEqual(len(statements), 2)
|
||||
self.assertEqual(len(statements[0].line_ids), 1)
|
||||
self.assertEqual(statements[0].balance_start, 0.0)
|
||||
self.assertEqual(statements[0].balance_end_real, 50.0)
|
||||
self.assertEqual(len(statements[1].line_ids), 1)
|
||||
self.assertEqual(statements[1].balance_start, 50.0)
|
||||
self.assertEqual(statements[1].balance_end_real, 100.0)
|
||||
|
||||
def test_weekly_import_mode(self):
|
||||
journal = self.AccountJournal.create({
|
||||
'name': 'Bank',
|
||||
'type': 'bank',
|
||||
'code': 'BANK',
|
||||
'currency_id': self.currency_usd.id,
|
||||
})
|
||||
wizard = self.AccountBankStatementImport.with_context({
|
||||
'journal_id': journal.id,
|
||||
}).create({
|
||||
'filename': 'file.ext',
|
||||
'data_file': self.empty_data_file,
|
||||
'import_mode': 'weekly',
|
||||
})
|
||||
data = (
|
||||
journal.currency_id.name,
|
||||
journal.bank_account_id.acc_number,
|
||||
[{
|
||||
'name': 'STATEMENT',
|
||||
'date': '2019-01-01',
|
||||
'balance_start': 0.0,
|
||||
'balance_end_real': 100.0,
|
||||
'transactions': [{
|
||||
'name': 'TRANSACTION-1',
|
||||
'amount': '50.0',
|
||||
'date': '2019-01-01',
|
||||
'note': 'NOTE',
|
||||
'unique_import_id': 'TRANSACTION-ID-1',
|
||||
}, {
|
||||
'name': 'TRANSACTION-2',
|
||||
'amount': '50.0',
|
||||
'date': '2019-01-15',
|
||||
'note': 'NOTE',
|
||||
'unique_import_id': 'TRANSACTION-ID-2',
|
||||
}],
|
||||
}],
|
||||
)
|
||||
with mock.patch(_parse_file_method, return_value=data):
|
||||
wizard.with_context({
|
||||
'journal_id': journal.id,
|
||||
}).import_file()
|
||||
statements = self.AccountBankStatement.search([
|
||||
('journal_id', '=', journal.id),
|
||||
]).sorted(key=lambda statement: statement.date)
|
||||
self.assertEqual(len(statements), 2)
|
||||
self.assertEqual(len(statements[0].line_ids), 1)
|
||||
self.assertEqual(statements[0].balance_start, 0.0)
|
||||
self.assertEqual(statements[0].balance_end_real, 50.0)
|
||||
self.assertEqual(len(statements[1].line_ids), 1)
|
||||
self.assertEqual(statements[1].balance_start, 50.0)
|
||||
self.assertEqual(statements[1].balance_end_real, 100.0)
|
||||
|
||||
def test_monthly_import_mode(self):
|
||||
journal = self.AccountJournal.create({
|
||||
'name': 'Bank',
|
||||
'type': 'bank',
|
||||
'code': 'BANK',
|
||||
'currency_id': self.currency_usd.id,
|
||||
})
|
||||
wizard = self.AccountBankStatementImport.with_context({
|
||||
'journal_id': journal.id,
|
||||
}).create({
|
||||
'filename': 'file.ext',
|
||||
'data_file': self.empty_data_file,
|
||||
'import_mode': 'monthly',
|
||||
})
|
||||
data = (
|
||||
journal.currency_id.name,
|
||||
journal.bank_account_id.acc_number,
|
||||
[{
|
||||
'name': 'STATEMENT',
|
||||
'date': '2019-01-01',
|
||||
'balance_start': 0.0,
|
||||
'balance_end_real': 100.0,
|
||||
'transactions': [{
|
||||
'name': 'TRANSACTION-1',
|
||||
'amount': '50.0',
|
||||
'date': '2019-01-01',
|
||||
'note': 'NOTE',
|
||||
'unique_import_id': 'TRANSACTION-ID-1',
|
||||
}, {
|
||||
'name': 'TRANSACTION-2',
|
||||
'amount': '50.0',
|
||||
'date': '2019-03-01',
|
||||
'note': 'NOTE',
|
||||
'unique_import_id': 'TRANSACTION-ID-2',
|
||||
}],
|
||||
}],
|
||||
)
|
||||
with mock.patch(_parse_file_method, return_value=data):
|
||||
wizard.with_context({
|
||||
'journal_id': journal.id,
|
||||
}).import_file()
|
||||
statements = self.AccountBankStatement.search([
|
||||
('journal_id', '=', journal.id),
|
||||
]).sorted(key=lambda statement: statement.date)
|
||||
self.assertEqual(len(statements), 2)
|
||||
self.assertEqual(len(statements[0].line_ids), 1)
|
||||
self.assertEqual(statements[0].balance_start, 0.0)
|
||||
self.assertEqual(statements[0].balance_end_real, 50.0)
|
||||
self.assertEqual(len(statements[1].line_ids), 1)
|
||||
self.assertEqual(statements[1].balance_start, 50.0)
|
||||
self.assertEqual(statements[1].balance_end_real, 100.0)
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2019 Brainbean Apps (https://brainbeanapps.com)
|
||||
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="account_bank_statement_import_view" model="ir.ui.view">
|
||||
<field name="model">account.bank.statement.import</field>
|
||||
<field name="inherit_id" ref="account_bank_statement_import.account_bank_statement_import_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//ul[@id='statement_format']" position="after">
|
||||
<p>Please select how you'd like to split the imported statement file:</p>
|
||||
<field name="import_mode" widget="radio" required="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user