OCA-git-bot
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