Alexey Pelykh
5 years ago
9 changed files with 475 additions and 0 deletions
-
3account_bank_statement_import_split/__init__.py
-
21account_bank_statement_import_split/__manifest__.py
-
3account_bank_statement_import_split/models/__init__.py
-
153account_bank_statement_import_split/models/account_bank_statement_import.py
-
1account_bank_statement_import_split/readme/CONTRIBUTORS.rst
-
5account_bank_statement_import_split/readme/DESCRIPTION.rst
-
3account_bank_statement_import_split/tests/__init__.py
-
267account_bank_statement_import_split/tests/test_account_bank_statement_import_split.py
-
19account_bank_statement_import_split/views/account_bank_statement_import.xml
@ -0,0 +1,3 @@ |
|||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). |
||||
|
|
||||
|
from . import models |
@ -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, |
||||
|
} |
@ -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 |
@ -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> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue