Browse Source

Merge PR #264 into 12.0

Signed-off-by alexey-pelykh
12.0
OCA-git-bot 4 years ago
parent
commit
c38e317a2b
  1. 32
      account_bank_statement_import_txt_xlsx/__manifest__.py
  2. 25
      account_bank_statement_import_txt_xlsx/data/map_data.xml
  3. 53
      account_bank_statement_import_txt_xlsx/data/txt_map_data.xml
  4. 98
      account_bank_statement_import_txt_xlsx/migrations/12.0.2.0.0/post-migration.py
  5. 6
      account_bank_statement_import_txt_xlsx/models/__init__.py
  6. 38
      account_bank_statement_import_txt_xlsx/models/account_bank_statement_import.py
  7. 188
      account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_sheet_mapping.py
  8. 331
      account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_sheet_parser.py
  9. 136
      account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_txt_map.py
  10. 15
      account_bank_statement_import_txt_xlsx/models/account_journal.py
  11. 14
      account_bank_statement_import_txt_xlsx/readme/CONFIGURE.rst
  12. 1
      account_bank_statement_import_txt_xlsx/readme/CONTRIBUTORS.rst
  13. 7
      account_bank_statement_import_txt_xlsx/readme/HISTORY.rst
  14. 3
      account_bank_statement_import_txt_xlsx/readme/USAGE.rst
  15. 4
      account_bank_statement_import_txt_xlsx/security/ir.model.access.csv
  16. 4
      account_bank_statement_import_txt_xlsx/tests/__init__.py
  17. 3
      account_bank_statement_import_txt_xlsx/tests/fixtures/balance.csv
  18. 3
      account_bank_statement_import_txt_xlsx/tests/fixtures/debit_credit.csv
  19. 1
      account_bank_statement_import_txt_xlsx/tests/fixtures/empty_statement_en.csv
  20. BIN
      account_bank_statement_import_txt_xlsx/tests/fixtures/empty_statement_en.xlsx
  21. 3
      account_bank_statement_import_txt_xlsx/tests/fixtures/multi_currency.csv
  22. 2
      account_bank_statement_import_txt_xlsx/tests/fixtures/original_currency.csv
  23. 0
      account_bank_statement_import_txt_xlsx/tests/fixtures/sample_statement_en.csv
  24. 0
      account_bank_statement_import_txt_xlsx/tests/fixtures/sample_statement_en.xlsx
  25. 327
      account_bank_statement_import_txt_xlsx/tests/test_account_bank_statement_import_txt_xlsx.py
  26. 99
      account_bank_statement_import_txt_xlsx/tests/test_txt_statement_import.py
  27. 10
      account_bank_statement_import_txt_xlsx/views/account_bank_statement_import.xml
  28. 82
      account_bank_statement_import_txt_xlsx/views/account_bank_statement_import_sheet_mapping.xml
  29. 4
      account_bank_statement_import_txt_xlsx/views/account_journal_views.xml
  30. 70
      account_bank_statement_import_txt_xlsx/views/txt_map_views.xml
  31. 5
      account_bank_statement_import_txt_xlsx/wizards/__init__.py
  32. 191
      account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_sheet_mapping_wizard.py
  33. 144
      account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_sheet_mapping_wizard.xml
  34. 288
      account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_txt.py
  35. 40
      account_bank_statement_import_txt_xlsx/wizards/create_map_lines_from_file.py
  36. 29
      account_bank_statement_import_txt_xlsx/wizards/create_map_lines_from_file_views.xml

32
account_bank_statement_import_txt_xlsx/__manifest__.py

@ -1,28 +1,36 @@
# Copyright 2014-2017 Akretion (http://www.akretion.com).
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
# Copyright 2019 ForgeFlow, S.L.
# Copyright 2020 Brainbean Apps (https://brainbeanapps.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Account Bank Statement Import TXT XLSX",
'summary': 'Import TXT/CSV or XLSX files as Bank Statements in Odoo',
"version": "12.0.1.0.1",
"summary": "Import TXT/CSV or XLSX files as Bank Statements in Odoo",
"version": "12.0.2.0.0",
"category": "Accounting",
"website": "https://github.com/OCA/bank-statement-import",
"author": " Eficent, Odoo Community Association (OCA)",
"author":
"ForgeFlow, "
"Brainbean Apps, "
"Odoo Community Association (OCA)",
"license": "AGPL-3",
"installable": True,
"depends": [
"account_bank_statement_import",
"multi_step_wizard",
"web_widget_dropdown_dynamic",
],
"external_dependencies": {
"python": ["xlrd"],
"python": [
"csv",
"xlrd",
]
},
"data": [
"security/ir.model.access.csv",
"data/txt_map_data.xml",
"wizards/create_map_lines_from_file_views.xml",
"wizards/account_bank_statement_import_view.xml",
"data/map_data.xml",
"views/account_bank_statement_import_sheet_mapping.xml",
"views/account_bank_statement_import.xml",
"views/account_journal_views.xml",
"views/txt_map_views.xml",
"wizards/account_bank_statement_import_sheet_mapping_wizard.xml",
]
}

25
account_bank_statement_import_txt_xlsx/data/map_data.xml

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 ForgeFlow, S.L.
Copyright 2020 Brainbean Apps (https://brainbeanapps.com)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo noupdate="1">
<record id="sample_statement_map" model="account.bank.statement.import.sheet.mapping">
<field name="name">Sample Statement</field>
<field name="float_thousands_sep">comma</field>
<field name="float_decimal_sep">dot</field>
<field name="delimiter">comma</field>
<field name="quotechar">"</field>
<field name="timestamp_format">%m/%d/%Y</field>
<field name="timestamp_column">Date</field>
<field name="amount_column">Amount</field>
<field name="original_currency_column">Currency</field>
<field name="original_amount_column">Amount Currency</field>
<field name="description_column">Label</field>
<field name="partner_name_column">Partner Name</field>
<field name="bank_account_column">Bank Account</field>
</record>
</odoo>

53
account_bank_statement_import_txt_xlsx/data/txt_map_data.xml

@ -1,53 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="txt_map" model="account.bank.statement.import.map">
<field name="name">Sample Statement</field>
<field name="float_thousands_sep">comma</field>
<field name="float_decimal_sep">dot</field>
</record>
<record id="txt_map_line_date" model="account.bank.statement.import.map.line">
<field name="name">Date</field>
<field name="sequence">0</field>
<field name="map_parent_id" ref="txt_map"/>
<field name="field_to_assign">date</field>
<field name="date_format">%m/%d/%Y</field>
</record>
<record id="txt_map_line_name" model="account.bank.statement.import.map.line">
<field name="name">Label</field>
<field name="sequence">1</field>
<field name="map_parent_id" ref="txt_map"/>
<field name="field_to_assign">name</field>
</record>
<record id="txt_map_line_currency" model="account.bank.statement.import.map.line">
<field name="name">Currency</field>
<field name="sequence">2</field>
<field name="map_parent_id" ref="txt_map"/>
<field name="field_to_assign">currency</field>
</record>
<record id="txt_map_line_amount" model="account.bank.statement.import.map.line">
<field name="name">Amount</field>
<field name="sequence">3</field>
<field name="map_parent_id" ref="txt_map"/>
<field name="field_to_assign">amount</field>
</record>
<record id="txt_map_line_amount_currency" model="account.bank.statement.import.map.line">
<field name="name">Amount Currency</field>
<field name="sequence">4</field>
<field name="map_parent_id" ref="txt_map"/>
<field name="field_to_assign">amount_currency</field>
</record>
<record id="txt_map_line_partner_name" model="account.bank.statement.import.map.line">
<field name="name">Partner Name</field>
<field name="sequence">5</field>
<field name="map_parent_id" ref="txt_map"/>
<field name="field_to_assign">partner_name</field>
</record>
<record id="txt_map_line_account_number" model="account.bank.statement.import.map.line">
<field name="name">Bank Account</field>
<field name="sequence">6</field>
<field name="map_parent_id" ref="txt_map"/>
<field name="field_to_assign">account_number</field>
</record>
</odoo>

98
account_bank_statement_import_txt_xlsx/migrations/12.0.2.0.0/post-migration.py

@ -0,0 +1,98 @@
# Copyright 2020 Brainbean Apps (https://brainbeanapps.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from openupgradelib import openupgrade
@openupgrade.migrate()
def migrate(env, version):
openupgrade.logged_query(
env.cr,
"""
WITH _mappings AS (
SELECT
m.id,
l.field_to_assign,
l.name,
l.date_format
FROM
account_bank_statement_import_map AS m
RIGHT OUTER JOIN (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY map_parent_id, field_to_assign
ORDER BY id ASC
) AS row_number
FROM account_bank_statement_import_map_line
WHERE field_to_assign IS NOT NULL
) AS l ON m.id = l.map_parent_id AND l.row_number = 1
)
INSERT INTO account_bank_statement_import_sheet_mapping (
name,
float_thousands_sep,
float_decimal_sep,
file_encoding,
delimiter,
quotechar,
timestamp_format,
timestamp_column,
amount_column,
original_currency_column,
original_amount_column,
description_column,
reference_column,
notes_column,
partner_name_column,
bank_account_column
)
SELECT
m.name,
m.float_thousands_sep,
m.float_decimal_sep,
m.file_encoding,
(
CASE
WHEN m.delimiter='.' THEN 'dot'
WHEN m.delimiter=',' THEN 'comma'
WHEN m.delimiter=';' THEN 'semicolon'
WHEN m.delimiter='' THEN 'n/a'
WHEN m.delimiter='\t' THEN 'tab'
WHEN m.delimiter=' ' THEN 'space'
ELSE 'n/a'
END
) AS delimiter,
m.quotechar,
COALESCE(_date.date_format, '%m/%d/%Y') AS timestamp_format,
COALESCE(_date.name, 'Date') AS timestamp_column,
COALESCE(_amount.name, 'Amount') AS amount_column,
_o_currency.name AS original_currency_column,
_o_amount.name AS original_amount_column,
_description.name AS description_column,
_ref.name AS reference_column,
_notes.name AS notes_column,
_p_name.name AS partner_name_column,
_bank_acc.name AS bank_account_column
FROM
account_bank_statement_import_map AS m
LEFT JOIN _mappings AS _date
ON m.id = _date.id AND _date.field_to_assign = 'date'
LEFT JOIN _mappings AS _description
ON m.id = _description.id AND _description.field_to_assign = 'name'
LEFT JOIN _mappings AS _o_currency
ON m.id = _o_currency.id AND _o_currency.field_to_assign = 'currency'
LEFT JOIN _mappings AS _amount
ON m.id = _amount.id AND _amount.field_to_assign = 'amount'
LEFT JOIN _mappings AS _o_amount
ON m.id = _o_amount.id AND _o_amount.field_to_assign = 'amount_currency'
LEFT JOIN _mappings AS _ref
ON m.id = _ref.id AND _ref.field_to_assign = 'ref'
LEFT JOIN _mappings AS _notes
ON m.id = _notes.id AND _notes.field_to_assign = 'note'
LEFT JOIN _mappings AS _p_name
ON m.id = _p_name.id AND _p_name.field_to_assign = 'partner_name'
LEFT JOIN _mappings AS _bank_acc
ON m.id = _bank_acc.id AND _bank_acc.field_to_assign = 'account_number';
"""
)

6
account_bank_statement_import_txt_xlsx/models/__init__.py

@ -1,2 +1,6 @@
from . import account_bank_statement_import_txt_map
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import account_bank_statement_import_sheet_mapping
from . import account_bank_statement_import_sheet_parser
from . import account_bank_statement_import
from . import account_journal

38
account_bank_statement_import_txt_xlsx/models/account_bank_statement_import.py

@ -0,0 +1,38 @@
# Copyright 2020 Brainbean Apps (https://brainbeanapps.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
import logging
_logger = logging.getLogger(__name__)
class AccountBankStatementImport(models.TransientModel):
_inherit = 'account.bank.statement.import'
def _get_default_mapping_id(self):
return self.env["account.journal"].browse(
self.env.context.get('journal_id')).default_sheet_mapping_id
sheet_mapping_id = fields.Many2one(
string='Sheet mapping',
comodel_name='account.bank.statement.import.sheet.mapping',
default=_get_default_mapping_id,
)
@api.multi
def _parse_file(self, data_file):
self.ensure_one()
try:
Parser = self.env['account.bank.statement.import.sheet.parser']
return Parser.parse(
self.sheet_mapping_id,
data_file,
self.filename
)
except:
if self.env.context.get(
'account_bank_statement_import_txt_xlsx_test'):
raise
_logger.warning('Sheet parser error', exc_info=True)
return super()._parse_file(data_file)

188
account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_sheet_mapping.py

@ -0,0 +1,188 @@
# Copyright 2019 ForgeFlow, S.L.
# Copyright 2020 Brainbean Apps (https://brainbeanapps.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountBankStatementImportSheetMapping(models.Model):
_name = 'account.bank.statement.import.sheet.mapping'
_description = 'Account Bank Statement Import Sheet Mapping'
name = fields.Char(
required=True,
)
float_thousands_sep = fields.Selection(
string='Thousands Separator',
selection=[
('dot', 'dot (.)'),
('comma', 'comma (,)'),
('none', 'none'),
],
default='dot',
)
float_decimal_sep = fields.Selection(
string='Decimals Separator',
selection=[
('dot', 'dot (.)'),
('comma', 'comma (,)'),
('none', 'none'),
],
default='comma',
)
file_encoding = fields.Selection(
string='Encoding',
selection=[
('utf-8', 'UTF-8'),
('utf-8-sig', 'UTF-8 (with BOM)'),
('utf-16', 'UTF-16'),
('utf-16-sig', 'UTF-16 (with BOM)'),
('windows-1252', 'Western (Windows-1252)'),
('iso-8859-1', 'Western (Latin-1 / ISO 8859-1)'),
('iso-8859-2', 'Central European (Latin-2 / ISO 8859-2)'),
('iso-8859-4', 'Baltic (Latin-4 / ISO 8859-4)'),
('big5', 'Traditional Chinese (big5)'),
('gb18030', 'Unified Chinese (gb18030)'),
('shift_jis', 'Japanese (Shift JIS)'),
('windows-1251', 'Cyrillic (Windows-1251)'),
('koi8_r', 'Cyrillic (KOI8-R)'),
('koi8_u', 'Cyrillic (KOI8-U)'),
],
default='utf-8',
)
delimiter = fields.Selection(
string='Delimiter',
selection=[
('dot', 'dot (.)'),
('comma', 'comma (,)'),
('semicolon', 'semicolon (;)'),
('tab', 'tab'),
('space', 'space'),
('n/a', 'N/A'),
],
default='comma',
)
quotechar = fields.Char(
string='Text qualifier',
size=1,
default='"',
)
timestamp_format = fields.Char(
string='Timestamp Format',
required=True,
)
timestamp_column = fields.Char(
string='Timestamp column',
required=True,
)
currency_column = fields.Char(
string='Currency column',
help=(
'In case statement is multi-currency, column to get currency of '
'transaction from'
),
)
amount_column = fields.Char(
string='Amount column',
required=True,
help='Amount of transaction in journal\'s currency',
)
balance_column = fields.Char(
string='Balance column',
help='Balance after transaction in journal\'s currency',
)
original_currency_column = fields.Char(
string='Original currency column',
help=(
'In case statement provides original currency for transactions '
'with automatic currency conversion, column to get original '
'currency of transaction from'
),
)
original_amount_column = fields.Char(
string='Original amount column',
help=(
'In case statement provides original currency for transactions '
'with automatic currency conversion, column to get original '
'transaction amount in original transaction currency from'
),
)
debit_credit_column = fields.Char(
string='Debit/credit column',
help=(
'Some statement formats use absolute amount value and indicate sign'
'of the transaction by specifying if it was a debit or a credit one'
),
)
debit_value = fields.Char(
string='Debit value',
help='Value of debit/credit column that indicates if it\'s a debit',
default='D',
)
credit_value = fields.Char(
string='Credit value',
help='Value of debit/credit column that indicates if it\'s a credit',
default='C',
)
transaction_id_column = fields.Char(
string='Unique transaction ID column',
)
description_column = fields.Char(
string='Description column',
)
notes_column = fields.Char(
string='Notes column',
)
reference_column = fields.Char(
string='Reference column',
)
partner_name_column = fields.Char(
string='Partner Name column',
)
bank_name_column = fields.Char(
string='Bank Name column',
help='Partner\'s bank',
)
bank_account_column = fields.Char(
string='Bank Account column',
help='Partner\'s bank account',
)
@api.onchange('float_thousands_sep')
def onchange_thousands_separator(self):
if 'dot' == self.float_thousands_sep == self.float_decimal_sep:
self.float_decimal_sep = 'comma'
elif 'comma' == self.float_thousands_sep == self.float_decimal_sep:
self.float_decimal_sep = 'dot'
@api.onchange('float_decimal_sep')
def onchange_decimal_separator(self):
if 'dot' == self.float_thousands_sep == self.float_decimal_sep:
self.float_thousands_sep = 'comma'
elif 'comma' == self.float_thousands_sep == self.float_decimal_sep:
self.float_thousands_sep = 'dot'
@api.multi
def _get_float_separators(self):
self.ensure_one()
separators = {
'dot': '.',
'comma': ',',
'none': '',
}
return (separators[self.float_thousands_sep],
separators[self.float_decimal_sep])
@api.model
def _decode_column_delimiter_character(self, delimiter):
return ({
'dot': '.',
'comma': ',',
'semicolon': ';',
'tab': '\t',
'space': ' ',
}).get(delimiter)
@api.multi
def _get_column_delimiter_character(self):
return self._decode_column_delimiter_character(self.delimiter)

331
account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_sheet_parser.py

@ -0,0 +1,331 @@
# Copyright 2019 ForgeFlow, S.L.
# Copyright 2020 Brainbean Apps (https://brainbeanapps.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, models, _
from datetime import datetime
from decimal import Decimal
from io import StringIO
from os import path
import itertools
import logging
_logger = logging.getLogger(__name__)
try:
from csv import reader
import xlrd
from xlrd.xldate import xldate_as_datetime
except (ImportError, IOError) as err: # pragma: no cover
_logger.error(err)
class AccountBankStatementImportSheetParser(models.TransientModel):
_name = 'account.bank.statement.import.sheet.parser'
_description = 'Account Bank Statement Import Sheet Parser'
@api.model
def parse_header(self, data_file, encoding, csv_options):
try:
workbook = xlrd.open_workbook(
file_contents=data_file,
encoding_override=encoding if encoding else None,
)
sheet = workbook.sheet_by_index(0)
values = sheet.row_values(0)
return [str(value) for value in values]
except xlrd.XLRDError:
pass
data = StringIO(data_file.decode(encoding or 'utf-8'))
csv_data = reader(data, **csv_options)
return list(next(csv_data))
@api.model
def parse(self, mapping, data_file, filename):
journal = self.env['account.journal'].browse(
self.env.context.get('journal_id')
)
currency_code = (
journal.currency_id or journal.company_id.currency_id
).name
account_number = journal.bank_account_id.acc_number
name = _('%s: %s') % (
journal.code,
path.basename(filename),
)
lines = self._parse_lines(mapping, data_file, currency_code)
if not lines:
return currency_code, account_number, [{
'name': name,
'transactions': [],
}]
lines = list(sorted(
lines,
key=lambda line: line['timestamp']
))
first_line = lines[0]
last_line = lines[-1]
data = {
'name': name,
'date': first_line['timestamp'].date(),
}
if mapping.balance_column:
balance_start = first_line['balance']
balance_start -= first_line['amount']
balance_end = last_line['balance']
data.update({
'balance_start': float(balance_start),
'balance_end_real': float(balance_end),
})
transactions = list(itertools.chain.from_iterable(map(
lambda line: self._convert_line_to_transactions(line),
lines
)))
data.update({
'transactions': transactions,
})
return currency_code, account_number, [data]
def _parse_lines(self, mapping, data_file, currency_code):
try:
workbook = xlrd.open_workbook(
file_contents=data_file,
encoding_override=(
mapping.file_encoding if mapping.file_encoding else None
),
)
csv_or_xlsx = (workbook, workbook.sheet_by_index(0),)
except xlrd.XLRDError:
csv_options = {}
csv_delimiter = mapping._get_column_delimiter_character()
if csv_delimiter:
csv_options['delimiter'] = csv_delimiter
if mapping.quotechar:
csv_options['quotechar'] = mapping.quotechar
csv_or_xlsx = reader(
StringIO(data_file.decode(mapping.file_encoding or 'utf-8')),
**csv_options
)
if isinstance(csv_or_xlsx, tuple):
header = [str(value) for value in csv_or_xlsx[1].row_values(0)]
else:
header = [value.strip() for value in next(csv_or_xlsx)]
timestamp_column = header.index(mapping.timestamp_column)
currency_column = header.index(mapping.currency_column) \
if mapping.currency_column else None
amount_column = header.index(mapping.amount_column)
balance_column = header.index(mapping.balance_column) \
if mapping.balance_column else None
original_currency_column = (
header.index(mapping.original_currency_column)
if mapping.original_currency_column else None
)
original_amount_column = (
header.index(mapping.original_amount_column)
if mapping.original_amount_column else None
)
debit_credit_column = header.index(mapping.debit_credit_column) \
if mapping.debit_credit_column else None
transaction_id_column = header.index(mapping.transaction_id_column) \
if mapping.transaction_id_column else None
description_column = header.index(mapping.description_column) \
if mapping.description_column else None
notes_column = header.index(mapping.notes_column) \
if mapping.notes_column else None
reference_column = header.index(mapping.reference_column) \
if mapping.reference_column else None
partner_name_column = header.index(mapping.partner_name_column) \
if mapping.partner_name_column else None
bank_name_column = header.index(mapping.bank_name_column) \
if mapping.bank_name_column else None
bank_account_column = header.index(mapping.bank_account_column) \
if mapping.bank_account_column else None
if isinstance(csv_or_xlsx, tuple):
rows = range(1, csv_or_xlsx[1].nrows)
else:
rows = csv_or_xlsx
lines = []
for row in rows:
if isinstance(csv_or_xlsx, tuple):
book = csv_or_xlsx[0]
sheet = csv_or_xlsx[1]
values = []
for col_index in range(sheet.row_len(row)):
cell_type = sheet.cell_type(row, col_index)
cell_value = sheet.cell_value(row, col_index)
if cell_type == xlrd.XL_CELL_DATE:
cell_value = xldate_as_datetime(cell_value, book.datemode)
values.append(cell_value)
else:
values = list(row)
timestamp = values[timestamp_column]
currency = values[currency_column] \
if currency_column is not None else currency_code
amount = values[amount_column]
balance = values[balance_column] \
if balance_column is not None else None
original_currency = values[original_currency_column] \
if original_currency_column is not None else None
original_amount = values[original_amount_column] \
if original_amount_column is not None else None
debit_credit = values[debit_credit_column] \
if debit_credit_column is not None else None
transaction_id = values[transaction_id_column] \
if transaction_id_column is not None else None
description = values[description_column] \
if description_column is not None else None
notes = values[notes_column] \
if notes_column is not None else None
reference = values[reference_column] \
if reference_column is not None else None
partner_name = values[partner_name_column] \
if partner_name_column is not None else None
bank_name = values[bank_name_column] \
if bank_name_column is not None else None
bank_account = values[bank_account_column] \
if bank_account_column is not None else None
if currency != currency_code:
continue
if isinstance(timestamp, str):
timestamp = datetime.strptime(
timestamp,
mapping.timestamp_format
)
amount = self._parse_decimal(amount, mapping)
if balance is not None:
balance = self._parse_decimal(balance, mapping)
if debit_credit is not None:
amount = amount.copy_abs()
if debit_credit == mapping.debit_value:
amount = -amount
if original_currency is None:
original_currency = currency
original_amount = amount
elif original_currency == currency:
original_amount = amount
original_amount = self._parse_decimal(original_amount, mapping)
line = {
'timestamp': timestamp,
'amount': amount,
'currency': currency,
'original_amount': original_amount,
'original_currency': original_currency,
}
if balance is not None:
line['balance'] = balance
if transaction_id is not None:
line['transaction_id'] = transaction_id
if description is not None:
line['description'] = description
if notes is not None:
line['notes'] = notes
if reference is not None:
line['reference'] = reference
if partner_name is not None:
line['partner_name'] = partner_name
if bank_name is not None:
line['bank_name'] = bank_name
if bank_account is not None:
line['bank_account'] = bank_account
lines.append(line)
return lines
@api.model
def _convert_line_to_transactions(self, line):
"""Hook for extension"""
timestamp = line['timestamp']
amount = line['amount']
currency = line['currency']
original_amount = line['original_amount']
original_currency = line['original_currency']
transaction_id = line.get('transaction_id')
description = line.get('description')
notes = line.get('notes')
reference = line.get('reference')
partner_name = line.get('partner_name')
bank_name = line.get('bank_name')
bank_account = line.get('bank_account')
transaction = {
'date': timestamp,
'amount': str(amount),
}
if currency != original_currency:
original_currency = self.env['res.currency'].search(
[('name', '=', original_currency)],
limit=1,
)
if original_currency:
transaction.update({
'amount_currency': str(original_amount),
'currency_id': original_currency.id,
})
if transaction_id:
transaction['unique_import_id'] = '%s-%s' % (
transaction_id,
int(timestamp.timestamp()),
)
transaction['name'] = description or _('N/A')
if reference:
transaction['ref'] = reference
note = ''
if bank_name:
note += _('Bank: %s; ') % (
bank_name,
)
if bank_account:
note += _('Account: %s; ') % (
bank_account,
)
if transaction_id:
note += _('Transaction ID: %s; ') % (
transaction_id,
)
if note and notes:
note = '%s\n%s' % (
note,
note.strip(),
)
elif note:
note = note.strip()
if note:
transaction['note'] = note
if partner_name:
transaction['partner_name'] = partner_name
if bank_account:
transaction['account_number'] = bank_account
return [transaction]
@api.model
def _parse_decimal(self, value, mapping):
if isinstance(value, Decimal):
return value
elif isinstance(value, float):
return Decimal(value)
thousands, decimal = mapping._get_float_separators()
value = value.replace(thousands, '')
value = value.replace(decimal, '.')
return Decimal(value)

136
account_bank_statement_import_txt_xlsx/models/account_bank_statement_import_txt_map.py

@ -1,136 +0,0 @@
# Copyright 2019 Tecnativa - Vicent Cubells
# Copyright 2019 Eficent Business and IT Consulting Services, S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models, api
class AccountBankStatementImportTxtMap(models.Model):
_name = 'account.bank.statement.import.map'
_description = 'Account Bank Statement Import Txt Map'
name = fields.Char(
required=True,
)
map_line_ids = fields.One2many(
comodel_name='account.bank.statement.import.map.line',
inverse_name='map_parent_id',
string="Map lines",
required=True,
copy=True,
)
float_thousands_sep = fields.Selection(
[('dot', 'dot (.)'),
('comma', 'comma (,)'),
('none', 'none'),
],
string='Thousands separator',
# forward compatibility: this was the value assumed
# before the field was added.
default='dot',
required=True
)
float_decimal_sep = fields.Selection(
[('dot', 'dot (.)'),
('comma', 'comma (,)'),
('none', 'none'),
],
string='Decimals separator',
# forward compatibility: this was the value assumed
# before the field was added.
default='comma',
required=True
)
file_encoding = fields.Selection(
string='Encoding',
selection=[
('utf-8', 'UTF-8'),
('utf-16 ', 'UTF-16'),
('windows-1252', 'Windows-1252'),
('latin1', 'latin1'),
('latin2', 'latin2'),
('big5', 'big5'),
('gb18030', 'gb18030'),
('shift_jis', 'shift_jis'),
('windows-1251', 'windows-1251'),
('koir8_r', 'koir9_r'),
],
default='utf-8',
)
delimiter = fields.Selection(
string='Separated by',
selection=[
('.', 'dot (.)'),
(',', 'comma (,)'),
(';', 'semicolon (;)'),
('', 'none'),
('\t', 'Tab'),
(' ', 'Space'),
],
default=',',
)
quotechar = fields.Char(string='String delimiter', size=1,
default='"')
@api.onchange('float_thousands_sep')
def onchange_thousands_separator(self):
if 'dot' == self.float_thousands_sep == self.float_decimal_sep:
self.float_decimal_sep = 'comma'
elif 'comma' == self.float_thousands_sep == self.float_decimal_sep:
self.float_decimal_sep = 'dot'
@api.onchange('float_decimal_sep')
def onchange_decimal_separator(self):
if 'dot' == self.float_thousands_sep == self.float_decimal_sep:
self.float_thousands_sep = 'comma'
elif 'comma' == self.float_thousands_sep == self.float_decimal_sep:
self.float_thousands_sep = 'dot'
def _get_separators(self):
separators = {'dot': '.',
'comma': ',',
'none': '',
}
return (separators[self.float_thousands_sep],
separators[self.float_decimal_sep])
class AccountBankStatementImportTxtMapLine(models.Model):
_name = 'account.bank.statement.import.map.line'
_description = 'Account Bank Statement Import Txt Map Line'
_order = "sequence asc, id asc"
sequence = fields.Integer(
string="Field number",
required=True,
)
name = fields.Char(
string="Header Name",
required=True,
)
map_parent_id = fields.Many2one(
comodel_name='account.bank.statement.import.map',
required=True,
ondelete='cascade',
)
field_to_assign = fields.Selection(
selection=[
('date', 'Date'),
('name', 'Label'),
('currency', 'Currency'),
('amount', 'Amount in the journal currency'),
('amount_currency', 'Amount in foreign currency'),
('ref', 'Reference'),
('note', 'Notes'),
('partner_name', 'Name'),
('account_number', 'Bank Account Number'),
],
string="Statement Field to Assign",
)
date_format = fields.Selection(
selection=[
('%d/%m/%Y', 'i.e. 15/12/2019'),
('%m/%d/%Y', 'i.e. 12/15/2019'),
],
string="Date Format",
)

15
account_bank_statement_import_txt_xlsx/models/account_journal.py

@ -1,19 +1,18 @@
# Copyright 2019 Tecnativa - Vicent Cubells
# Copyright 2019 Eficent Business and IT Consulting Services, S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
# Copyright 2019 ForgeFlow, S.L.
# Copyright 2020 Brainbean Apps (https://brainbeanapps.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class AccountJournal(models.Model):
_inherit = "account.journal"
_inherit = 'account.journal'
statement_import_txt_map_id = fields.Many2one(
comodel_name='account.bank.statement.import.map',
string='Statement Import Txt Map',
default_sheet_mapping_id = fields.Many2one(
comodel_name='account.bank.statement.import.sheet.mapping',
)
def _get_bank_statements_available_import_formats(self):
res = super()._get_bank_statements_available_import_formats()
res.append('Txt')
res.append('TXT/CSV/XSLX')
return res

14
account_bank_statement_import_txt_xlsx/readme/CONFIGURE.rst

@ -1,12 +1,4 @@
* Create or go to a bank journal where you want to import the txt statement.
* Edit that journal and set a Txt map in **Statement Import Map** section in **Advanced
Settings** tab.
To create TXT/CSV/XLSX statement sheet columns mapping:
* Now you can import Text based statements in that journal.
Note: if existent Txt Map does not fit to your file to import, you can
create another map in **Invoicing > Configuration > Accounting >
Statement Import Map**.
You can import headers from any Txt file in **Action > Create Map
Lines** and set every line with which field of statement have to match.
#. Open *Invoicing > Configuration > Accounting > Statement Sheet Mappings*
#. Create mapping(s) according to your online banking software statement format

1
account_bank_statement_import_txt_xlsx/readme/CONTRIBUTORS.rst

@ -5,3 +5,4 @@
* Victor M.M. Torres <victor.martin@tecnativa.com>
* Eficent (https://www.eficent.com)
* Jordi Ballester Alomar <jordi.ballester@eficent.com>
* Alexey Pelykh <alexey.pelykh@brainbeanapps.com>

7
account_bank_statement_import_txt_xlsx/readme/HISTORY.rst

@ -0,0 +1,7 @@
12.0.2.0.0
~~~~~~~~~~
* [BREAKING] New mapping, please review mappings after upgrade.
* [BREAKING] Different bank accounts have to be used per each currency.
* [ADD] Support for both Statement and Activity reports.
* [ADD] Separate fee and currency exchange parsing.

3
account_bank_statement_import_txt_xlsx/readme/USAGE.rst

@ -1,3 +1,4 @@
To use this module, you need to:
#. Go to your bank online and download your Bank Statement in TXT/CSV or XLSX format.
#. Get statement in TXT/CSV or XLSX from your online banking software
#. Go to Odoo and and import the statement file, selecting corresponding format

4
account_bank_statement_import_txt_xlsx/security/ir.model.access.csv

@ -1,3 +1,3 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
access_account_bank_statement_import_map,map manager,model_account_bank_statement_import_map,account.group_account_manager,1,1,1,1
access_account_bank_statement_import_map_line,map line manager,model_account_bank_statement_import_map_line,account.group_account_manager,1,1,1,1
access_account_bank_statement_import_sheet_mapping_manager,account.bank.statement.import.sheet.mapping:account.group_account_manager,model_account_bank_statement_import_sheet_mapping,account.group_account_manager,1,1,1,1
access_account_bank_statement_import_sheet_mapping_user,account.bank.statement.import.sheet.mapping:account.group_account_user,model_account_bank_statement_import_sheet_mapping,account.group_account_user,1,0,0,0

4
account_bank_statement_import_txt_xlsx/tests/__init__.py

@ -1 +1,3 @@
from . import test_txt_statement_import
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import test_account_bank_statement_import_txt_xlsx

3
account_bank_statement_import_txt_xlsx/tests/fixtures/balance.csv

@ -0,0 +1,3 @@
"Date","Label","Amount","Balance","Partner Name","Bank Account"
"12/15/2018","Your best supplier","-33.50","-23.50","John Doe","123456789"
"12/15/2018","Your payment","1,533.50","1,510.00","Azure Interior",""

3
account_bank_statement_import_txt_xlsx/tests/fixtures/debit_credit.csv

@ -0,0 +1,3 @@
"Date","Label","Amount","D/C","Balance","Partner Name","Bank Account"
"12/15/2018","Your best supplier","33.50","D","-23.50","John Doe","123456789"
"12/15/2018","Your payment","-1,533.50","C","1,510.00","Azure Interior",""

1
account_bank_statement_import_txt_xlsx/tests/fixtures/empty_statement_en.csv

@ -0,0 +1 @@
"Date","Label","Currency","Amount","Amount Currency","Partner Name","Bank Account"

BIN
account_bank_statement_import_txt_xlsx/tests/fixtures/empty_statement_en.xlsx

3
account_bank_statement_import_txt_xlsx/tests/fixtures/multi_currency.csv

@ -0,0 +1,3 @@
"Date","Label","Currency","Amount","Partner Name","Bank Account"
"12/15/2018","Your best supplier","USD","-33.50","John Doe","123456789"
"12/15/2018","Your payment","EUR","1,525.00","Azure Interior",""

2
account_bank_statement_import_txt_xlsx/tests/fixtures/original_currency.csv

@ -0,0 +1,2 @@
"Date","Label","Currency","Amount","Amount Currency","Partner Name","Bank Account"
"12/15/2018","Your payment","EUR","1,525.00","1,000.00","Azure Interior",""

0
account_bank_statement_import_txt_xlsx/tests/sample_statement_en.csv → account_bank_statement_import_txt_xlsx/tests/fixtures/sample_statement_en.csv

0
account_bank_statement_import_txt_xlsx/tests/sample_statement_en.xlsx → account_bank_statement_import_txt_xlsx/tests/fixtures/sample_statement_en.xlsx

327
account_bank_statement_import_txt_xlsx/tests/test_account_bank_statement_import_txt_xlsx.py

@ -0,0 +1,327 @@
# Copyright 2019 ForgeFlow, S.L.
# Copyright 2020 Brainbean Apps (https://brainbeanapps.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields
from odoo.exceptions import UserError
from odoo.tests import common
from base64 import b64encode
from os import path
class TestAccountBankStatementImportTxtXlsx(common.TransactionCase):
def setUp(self):
super().setUp()
self.now = fields.Datetime.now()
self.currency_eur = self.env.ref('base.EUR')
self.currency_usd = self.env.ref('base.USD')
self.sample_statement_map = self.env.ref(
'account_bank_statement_import_txt_xlsx.sample_statement_map'
)
self.AccountJournal = self.env['account.journal']
self.AccountBankStatement = self.env['account.bank.statement']
self.AccountBankStatementImport = self.env[
'account.bank.statement.import'
]
self.AccountBankStatementImportSheetMapping = self.env[
'account.bank.statement.import.sheet.mapping'
]
self.AccountBankStatementImportSheetMappingWizard = self.env[
'account.bank.statement.import.sheet.mapping.wizard'
]
def _data_file(self, filename, encoding=None):
mode = 'rt' if encoding else 'rb'
with open(path.join(path.dirname(__file__), filename), mode) as file:
data = file.read()
if encoding:
data = data.encode(encoding)
return b64encode(data)
def test_import_csv_file(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': 'fixtures/sample_statement_en.csv',
'data_file': self._data_file(
'fixtures/sample_statement_en.csv',
'utf-8'
),
'sheet_mapping_id': self.sample_statement_map.id,
})
wizard.with_context({
'journal_id': journal.id,
'account_bank_statement_import_txt_xlsx_test': True,
}).import_file()
statement = self.AccountBankStatement.search([
('journal_id', '=', journal.id),
])
self.assertEqual(len(statement), 1)
self.assertEqual(len(statement.line_ids), 2)
def test_import_empty_csv_file(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': 'fixtures/empty_statement_en.csv',
'data_file': self._data_file(
'fixtures/empty_statement_en.csv',
'utf-8'
),
'sheet_mapping_id': self.sample_statement_map.id,
})
with self.assertRaises(UserError):
wizard.with_context({
'journal_id': journal.id,
'account_bank_statement_import_txt_xlsx_test': True,
}).import_file()
statement = self.AccountBankStatement.search([
('journal_id', '=', journal.id),
])
self.assertEqual(len(statement), 0)
def test_import_xlsx_file(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': 'fixtures/sample_statement_en.xlsx',
'data_file': self._data_file('fixtures/sample_statement_en.xlsx'),
'sheet_mapping_id': self.sample_statement_map.id,
})
wizard.with_context({
'journal_id': journal.id,
'account_bank_statement_import_txt_xlsx_test': True,
}).import_file()
statement = self.AccountBankStatement.search([
('journal_id', '=', journal.id),
])
self.assertEqual(len(statement), 1)
self.assertEqual(len(statement.line_ids), 2)
def test_import_empty_xlsx_file(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': 'fixtures/empty_statement_en.xlsx',
'data_file': self._data_file('fixtures/empty_statement_en.xlsx'),
'sheet_mapping_id': self.sample_statement_map.id,
})
with self.assertRaises(UserError):
wizard.with_context({
'journal_id': journal.id,
'account_bank_statement_import_txt_xlsx_test': True,
}).import_file()
statement = self.AccountBankStatement.search([
('journal_id', '=', journal.id),
])
self.assertEqual(len(statement), 0)
def test_mapping_import_wizard_xlsx(self):
with common.Form(
self.AccountBankStatementImportSheetMappingWizard) as form:
form.filename = 'fixtures/empty_statement_en.xlsx'
form.data_file = self._data_file(
'fixtures/empty_statement_en.xlsx'
)
self.assertEqual(len(form.header), 90)
self.assertEqual(
len(
self.AccountBankStatementImportSheetMappingWizard
.with_context(
header=form.header,
).statement_columns()
),
7
)
form.timestamp_column = 'Date'
form.amount_column = 'Amount'
wizard = form.save()
wizard.import_mapping()
def test_mapping_import_wizard_csv(self):
with common.Form(
self.AccountBankStatementImportSheetMappingWizard) as form:
form.filename = 'fixtures/empty_statement_en.csv'
form.data_file = self._data_file(
'fixtures/empty_statement_en.csv'
)
self.assertEqual(len(form.header), 90)
self.assertEqual(
len(
self.AccountBankStatementImportSheetMappingWizard
.with_context(
header=form.header,
).statement_columns()
),
7
)
form.timestamp_column = 'Date'
form.amount_column = 'Amount'
wizard = form.save()
wizard.import_mapping()
def test_original_currency(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': 'fixtures/original_currency.csv',
'data_file': self._data_file(
'fixtures/original_currency.csv',
'utf-8'
),
'sheet_mapping_id': self.sample_statement_map.id,
})
wizard.with_context({
'journal_id': journal.id,
'account_bank_statement_import_txt_xlsx_test': True,
}).import_file()
statement = self.AccountBankStatement.search([
('journal_id', '=', journal.id),
])
self.assertEqual(len(statement), 1)
self.assertEqual(len(statement.line_ids), 1)
line = statement.line_ids
self.assertEqual(line.currency_id, self.currency_eur)
self.assertEqual(line.amount_currency, 1000.0)
def test_multi_currency(self):
journal = self.AccountJournal.create({
'name': 'Bank',
'type': 'bank',
'code': 'BANK',
'currency_id': self.currency_usd.id,
})
statement_map = self.sample_statement_map.copy({
'currency_column': 'Currency',
'original_currency_column': None,
'original_amount_column': None,
})
wizard = self.AccountBankStatementImport.with_context({
'journal_id': journal.id,
}).create({
'filename': 'fixtures/multi_currency.csv',
'data_file': self._data_file(
'fixtures/multi_currency.csv',
'utf-8'
),
'sheet_mapping_id': statement_map.id,
})
wizard.with_context({
'journal_id': journal.id,
'account_bank_statement_import_txt_xlsx_test': True,
}).import_file()
statement = self.AccountBankStatement.search([
('journal_id', '=', journal.id),
])
self.assertEqual(len(statement), 1)
self.assertEqual(len(statement.line_ids), 1)
line = statement.line_ids
self.assertFalse(line.currency_id)
self.assertEqual(line.amount, -33.5)
def test_balance(self):
journal = self.AccountJournal.create({
'name': 'Bank',
'type': 'bank',
'code': 'BANK',
'currency_id': self.currency_usd.id,
})
statement_map = self.sample_statement_map.copy({
'balance_column': 'Balance',
'original_currency_column': None,
'original_amount_column': None,
})
wizard = self.AccountBankStatementImport.with_context({
'journal_id': journal.id,
}).create({
'filename': 'fixtures/balance.csv',
'data_file': self._data_file(
'fixtures/balance.csv',
'utf-8'
),
'sheet_mapping_id': statement_map.id,
})
wizard.with_context({
'journal_id': journal.id,
'account_bank_statement_import_txt_xlsx_test': True,
}).import_file()
statement = self.AccountBankStatement.search([
('journal_id', '=', journal.id),
])
self.assertEqual(len(statement), 1)
self.assertEqual(len(statement.line_ids), 2)
self.assertEqual(statement.balance_start, 10.0)
self.assertEqual(statement.balance_end_real, 1510.0)
self.assertEqual(statement.balance_end, 1510.0)
def test_debit_credit(self):
journal = self.AccountJournal.create({
'name': 'Bank',
'type': 'bank',
'code': 'BANK',
'currency_id': self.currency_usd.id,
})
statement_map = self.sample_statement_map.copy({
'balance_column': 'Balance',
'original_currency_column': None,
'original_amount_column': None,
'debit_credit_column': 'D/C',
'debit_value': 'D',
'credit_value': 'C',
})
wizard = self.AccountBankStatementImport.with_context({
'journal_id': journal.id,
}).create({
'filename': 'fixtures/debit_credit.csv',
'data_file': self._data_file(
'fixtures/debit_credit.csv',
'utf-8'
),
'sheet_mapping_id': statement_map.id,
})
wizard.with_context({
'journal_id': journal.id,
'account_bank_statement_import_txt_xlsx_test': True,
}).import_file()
statement = self.AccountBankStatement.search([
('journal_id', '=', journal.id),
])
self.assertEqual(len(statement), 1)
self.assertEqual(len(statement.line_ids), 2)
self.assertEqual(statement.balance_start, 10.0)
self.assertEqual(statement.balance_end_real, 1510.0)
self.assertEqual(statement.balance_end, 1510.0)

99
account_bank_statement_import_txt_xlsx/tests/test_txt_statement_import.py

@ -1,99 +0,0 @@
# Copyright 2019 Tecnativa - Vicent Cubells
# Copyright 2019 Eficent Business and IT Consulting Services, S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import os
import base64
from odoo.tests import common
class TestTxtFile(common.TransactionCase):
def setUp(self):
super(TestTxtFile, self).setUp()
self.map = self.env['account.bank.statement.import.map'].create({
'name': 'Txt Map Test',
})
usd = self.env.ref('base.USD')
self.journal = self.env['account.journal'].create({
'name': 'Txt Bank',
'type': 'bank',
'code': 'TXT',
'currency_id': (
usd.id if self.env.user.company_id.currency_id != usd
else False
),
})
def _do_import_xlsx(self, file_name):
file_name = os.path.join(os.path.dirname(__file__), file_name)
with open(file_name, 'rb') as fin:
data = fin.read()
return data
def _do_import(self, file_name):
file_name = os.path.join(os.path.dirname(__file__), file_name)
return open(file_name).read()
def test_import_header(self):
file = self._do_import('sample_statement_en.csv')
file = base64.b64encode(file.encode("utf-8"))
wizard = self.env['wizard.txt.map.create'].with_context({
'journal_id': self.journal.id,
'active_ids': [self.map.id],
}).create({'data_file': file})
wizard.create_map_lines()
self.assertEqual(len(self.map.map_line_ids.ids), 7)
def test_import_txt_file(self):
# Current statements before to run the wizard
old_statements = self.env['account.bank.statement'].search([])
# This journal is for Txt statements
txt_map = self.env.ref(
'account_bank_statement_import_txt_xlsx.txt_map'
)
self.journal.statement_import_txt_map_id = txt_map.id
file = self._do_import('sample_statement_en.csv')
file = base64.b64encode(file.encode("utf-8"))
wizard = self.env['account.bank.statement.import'].with_context({
'journal_id': self.journal.id,
}).create({'data_file': file})
wizard.import_file()
staments_now = self.env['account.bank.statement'].search([])
statement = staments_now - old_statements
self.assertEqual(len(statement.line_ids), 2)
self.assertEqual(len(statement.mapped('line_ids').filtered(
lambda x: x.partner_id)), 1)
self.assertAlmostEqual(
sum(statement.mapped('line_ids.amount')), 1491.50
)
self.assertAlmostEqual(
sum(statement.mapped('line_ids.amount_currency')), 1000.00
)
def test_import_xlsx_file(self):
# Current statements before to run the wizard
old_statements = self.env['account.bank.statement'].search([])
# This journal is for Txt statements
txt_map = self.env.ref(
'account_bank_statement_import_txt_xlsx.txt_map'
)
self.journal.statement_import_txt_map_id = txt_map.id
file = self._do_import_xlsx('sample_statement_en.xlsx')
file = base64.b64encode(file)
wizard = self.env['account.bank.statement.import'].with_context({
'journal_id': self.journal.id,
}).create({'data_file': file})
wizard.import_file()
staments_now = self.env['account.bank.statement'].search([])
statement = staments_now - old_statements
self.assertEqual(len(statement.line_ids), 2)
self.assertEqual(len(statement.mapped('line_ids').filtered(
lambda x: x.partner_id)), 1)
self.assertAlmostEqual(
sum(statement.mapped('line_ids.amount')), 1491.50
)
self.assertAlmostEqual(
sum(statement.mapped('line_ids.amount_currency')), 1000.00
)

10
account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_view.xml → account_bank_statement_import_txt_xlsx/views/account_bank_statement_import.xml

@ -1,12 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 ForgeFlow, S.L.
Copyright 2020 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="name">account.bank.statement.import</field>
<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="inside">
<li>Txt/XLSX file with Template: <field name="txt_map_id"/></li>
<li>
TXT/CSV/XLSX mapping: <field name="sheet_mapping_id" nolabel="1"/>
</li>
</xpath>
</field>
</record>

82
account_bank_statement_import_txt_xlsx/views/account_bank_statement_import_sheet_mapping.xml

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 ForgeFlow, S.L.
Copyright 2020 Brainbean Apps (https://brainbeanapps.com)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="account_bank_statement_import_sheet_mapping_form" model="ir.ui.view">
<field name="name">account.bank.statement.import.sheet.mapping.form</field>
<field name="model">account.bank.statement.import.sheet.mapping</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
</tree>
</field>
</record>
<record id="account_bank_statement_import_sheet_mapping_tree" model="ir.ui.view">
<field name="name">account.bank.statement.import.sheet.mapping.tree</field>
<field name="model">account.bank.statement.import.sheet.mapping</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1><field name="name"/></h1>
</div>
<group>
<group>
<field name="float_thousands_sep"/>
<field name="float_decimal_sep"/>
</group>
<group>
<field name="file_encoding"/>
<field name="delimiter"/>
<field name="quotechar"/>
</group>
<group>
<field name="timestamp_format"/>
</group>
<group attrs="{'invisible': [('debit_credit_column', '=', False)]}">
<field name="debit_value" attrs="{'required': [('debit_credit_column', '!=', False)]}"/>
<field name="credit_value" attrs="{'required': [('debit_credit_column', '!=', False)]}"/>
</group>
</group>
<group string="Columns">
<field name="timestamp_column"/>
<field name="currency_column"/>
<field name="amount_column"/>
<field name="balance_column"/>
<field name="original_currency_column"/>
<field name="original_amount_column"/>
<field name="debit_credit_column"/>
<field name="transaction_id_column"/>
<field name="description_column"/>
<field name="notes_column"/>
<field name="reference_column"/>
<field name="partner_name_column"/>
<field name="bank_name_column"/>
<field name="bank_account_column"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_statement_import_sheet_report_mappings" model="ir.actions.act_window">
<field name="name">Statement Sheet Mappings</field>
<field name="res_model">account.bank.statement.import.sheet.mapping</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem
id="menu_statement_import_sheet_mapping"
parent="account.account_account_menu"
action="action_statement_import_sheet_report_mappings"
name="Statement Sheet Mappings"
/>
</odoo>

4
account_bank_statement_import_txt_xlsx/views/account_journal_views.xml

@ -6,8 +6,8 @@
<field name="inherit_id" ref="account.view_account_journal_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='advanced_settings']/group" position="inside">
<group string="Statement Import Txt Map" attrs="{'invisible': [('type','!=','bank')]}">
<field name="statement_import_txt_map_id"/>
<group string="Statement Import Map" attrs="{'invisible': [('type','!=','bank')]}">
<field name="default_sheet_mapping_id"/>
</group>
</xpath>
</field>

70
account_bank_statement_import_txt_xlsx/views/txt_map_views.xml

@ -1,70 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="statement_import_map_tax_tree" model="ir.ui.view">
<field name="model">account.bank.statement.import.map</field>
<field name="arch" type="xml">
<tree string="Txt Map">
<field name="name"/>
</tree>
</field>
</record>
<record id="statement_import_map_tax_form" model="ir.ui.view">
<field name="model">account.bank.statement.import.map</field>
<field name="arch" type="xml">
<form string="Txt Map">
<group>
<field name="name"/>
<field name="float_thousands_sep"/>
<field name="float_decimal_sep"/>
<field name="file_encoding"/>
<field name="delimiter"/>
<field name="quotechar"/>
</group>
<separator string="Mapping Lines"/>
<field name="map_line_ids"/>
</form>
</field>
</record>
<record id="statement_import_map_tax_line_tree" model="ir.ui.view">
<field name="model">account.bank.statement.import.map.line</field>
<field name="arch" type="xml">
<tree string="Txt mapping lines" editable="bottom">
<field name="sequence"/>
<field name="name"/>
<field name="field_to_assign"/>
<field name="date_format" attrs="{'readonly':[('field_to_assign','!=','date')], 'required':[('field_to_assign','=','date')]}"/>
</tree>
</field>
</record>
<record id="statement_import_map_tax_line_form" model="ir.ui.view">
<field name="model">account.bank.statement.import.map.line</field>
<field name="arch" type="xml">
<form string="Txt mapping line">
<group>
<group>
<field name="name"/>
<field name="sequence"/>
<field name="field_to_assign"/>
</group>
</group>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="action_statement_import_txt_mappging">
<field name="name">Statement Import Mapping</field>
<field name="res_model">account.bank.statement.import.map</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="menu_statement_import_txt_mapping"
parent="account.account_account_menu"
action="action_statement_import_txt_mappging"
name="Statement Import Map"/>
</odoo>

5
account_bank_statement_import_txt_xlsx/wizards/__init__.py

@ -1,2 +1,3 @@
from . import create_map_lines_from_file
from . import account_bank_statement_import_txt
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import account_bank_statement_import_sheet_mapping_wizard

191
account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_sheet_mapping_wizard.py

@ -0,0 +1,191 @@
# Copyright 2019 ForgeFlow, S.L.
# Copyright 2020 Brainbean Apps (https://brainbeanapps.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models, _
from base64 import b64decode
import json
from os import path
class AccountBankStatementImportSheetMappingWizard(models.TransientModel):
_name = 'account.bank.statement.import.sheet.mapping.wizard'
_description = 'Account Bank Statement Import Sheet Mapping Wizard'
_inherit = ['multi.step.wizard.mixin']
data_file = fields.Binary(
string='Bank Statement File',
required=True,
)
filename = fields.Char()
header = fields.Char()
file_encoding = fields.Selection(
string='Encoding',
selection=lambda self: self._selection_file_encoding(),
)
delimiter = fields.Selection(
string='Delimiter',
selection=lambda self: self._selection_delimiter(),
)
quotechar = fields.Char(
string='Text qualifier',
size=1,
)
timestamp_column = fields.Char(
string='Timestamp column',
)
currency_column = fields.Char(
string='Currency column',
help=(
'In case statement is multi-currency, column to get currency of '
'transaction from'
),
)
amount_column = fields.Char(
string='Amount column',
help='Amount of transaction in journal\'s currency',
)
balance_column = fields.Char(
string='Balance column',
help='Balance after transaction in journal\'s currency',
)
original_currency_column = fields.Char(
string='Original currency column',
help=(
'In case statement provides original currency for transactions '
'with automatic currency conversion, column to get original '
'currency of transaction from'
),
)
original_amount_column = fields.Char(
string='Original amount column',
help=(
'In case statement provides original currency for transactions '
'with automatic currency conversion, column to get original '
'transaction amount in original transaction currency from'
),
)
debit_credit_column = fields.Char(
string='Debit/credit column',
help=(
'Some statement formats use absolute amount value and indicate sign'
'of the transaction by specifying if it was a debit or a credit one'
),
)
debit_value = fields.Char(
string='Debit value',
help='Value of debit/credit column that indicates if it\'s a debit',
default='D',
)
credit_value = fields.Char(
string='Credit value',
help='Value of debit/credit column that indicates if it\'s a credit',
default='C',
)
transaction_id_column = fields.Char(
string='Unique transaction ID column',
)
description_column = fields.Char(
string='Description column',
)
notes_column = fields.Char(
string='Notes column',
)
reference_column = fields.Char(
string='Reference column',
)
partner_name_column = fields.Char(
string='Partner Name column',
)
bank_name_column = fields.Char(
string='Bank Name column',
help='Partner\'s bank',
)
bank_account_column = fields.Char(
string='Bank Account column',
help='Partner\'s bank account',
)
@api.model
def _selection_file_encoding(self):
return self.env['account.bank.statement.import.sheet.mapping']._fields[
'file_encoding'
].selection
@api.model
def _selection_delimiter(self):
return self.env['account.bank.statement.import.sheet.mapping']._fields[
'delimiter'
].selection
@api.onchange('data_file')
def _onchange_data_file(self):
Parser = self.env['account.bank.statement.import.sheet.parser']
Mapping = self.env['account.bank.statement.import.sheet.mapping']
if not self.data_file:
return
csv_options = {}
if self.delimiter:
csv_options['delimiter'] = \
Mapping._decode_column_delimiter_character(self.delimiter)
if self.quotechar:
csv_options['quotechar'] = self.quotechar
header = Parser.parse_header(
b64decode(self.data_file),
self.file_encoding,
csv_options
)
self.header = json.dumps(header)
@api.model
def statement_columns(self):
header = self.env.context.get('header')
if not header:
return []
return [(x, x) for x in json.loads(header)]
@api.multi
def _get_mapping_values(self):
"""Hook for extension"""
self.ensure_one()
return {
'name': _('Mapping from %s') % path.basename(self.filename),
'float_thousands_sep': 'comma',
'float_decimal_sep': 'dot',
'file_encoding': self.file_encoding,
'delimiter': self.delimiter,
'quotechar': self.quotechar,
'timestamp_format': '%d/%m/%Y',
'timestamp_column': self.timestamp_column,
'currency_column': self.currency_column,
'amount_column': self.amount_column,
'balance_column': self.balance_column,
'original_currency_column': self.original_currency_column,
'original_amount_column': self.original_amount_column,
'debit_credit_column': self.debit_credit_column,
'debit_value': self.debit_value,
'credit_value': self.credit_value,
'transaction_id_column': self.transaction_id_column,
'description_column': self.description_column,
'notes_column': self.notes_column,
'reference_column': self.reference_column,
'partner_name_column': self.partner_name_column,
'bank_name_column': self.bank_name_column,
'bank_account_column': self.bank_account_column,
}
@api.multi
def import_mapping(self):
self.ensure_one()
mapping = self.env['account.bank.statement.import.sheet.mapping']\
.create(self._get_mapping_values())
return {
'type': 'ir.actions.act_window',
'name': _('Imported Mapping'),
'res_model': 'account.bank.statement.import.sheet.mapping',
'res_id': mapping.id,
'view_mode': 'form',
'view_id': False,
'target': 'new',
}

144
account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_sheet_mapping_wizard.xml

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 ForgeFlow, S.L.
Copyright 2020 Brainbean Apps (https://brainbeanapps.com)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="account_bank_statement_import_sheet_mapping_wizard_form" model="ir.ui.view">
<field name="name">account.bank.statement.import.sheet.mapping.wizard.form</field>
<field name="model">account.bank.statement.import.sheet.mapping.wizard</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="multi_step_wizard.multi_step_wizard_form"/>
<field name="arch" type="xml">
<xpath expr="//footer" position="before">
<h2>Select a statement file to import mapping</h2>
<group name="start" attrs="{'invisible': [('state', '!=', 'start')]}">
<group colspan="2">
<field name="data_file" filename="filename" placeholder="Choose a file to import..."/>
<field name="filename" invisible="1"/>
</group>
<group string="Options" colspan="2">
<field name="file_encoding"/>
<field name="delimiter"/>
<field name="quotechar"/>
</group>
</group>
<group name="final" attrs="{'invisible': [('state', '!=', 'final')]}">
<group colspan="2">
<field name="header" invisible="1"/>
<field
name="timestamp_column"
widget="dynamic_dropdown"
values="statement_columns"
context="{'header': header}"
attrs="{'required': [('state', '=', 'final')]}"
/>
<field
name="currency_column"
widget="dynamic_dropdown"
values="statement_columns"
context="{'header': header}"
/>
<field
name="amount_column"
widget="dynamic_dropdown"
values="statement_columns"
context="{'header': header}"
attrs="{'required': [('state', '=', 'final')]}"
/>
<field
name="balance_column"
widget="dynamic_dropdown"
values="statement_columns"
context="{'header': header}"
/>
<field
name="original_currency_column"
widget="dynamic_dropdown"
values="statement_columns"
context="{'header': header}"
/>
<field
name="original_amount_column"
widget="dynamic_dropdown"
values="statement_columns"
context="{'header': header}"
/>
<field
name="debit_credit_column"
widget="dynamic_dropdown"
values="statement_columns"
context="{'header': header}"
/>
<field
name="transaction_id_column"
widget="dynamic_dropdown"
values="statement_columns"
context="{'header': header}"
/>
<field
name="description_column"
widget="dynamic_dropdown"
values="statement_columns"
context="{'header': header}"
/>
<field
name="notes_column"
widget="dynamic_dropdown"
values="statement_columns"
context="{'header': header}"
/>
<field
name="reference_column"
widget="dynamic_dropdown"
values="statement_columns"
context="{'header': header}"
/>
<field
name="partner_name_column"
widget="dynamic_dropdown"
values="statement_columns"
context="{'header': header}"
/>
<field
name="bank_name_column"
widget="dynamic_dropdown"
values="statement_columns"
context="{'header': header}"
/>
<field
name="bank_account_column"
widget="dynamic_dropdown"
values="statement_columns"
context="{'header': header}"
/>
</group>
<group string="Debit/Credit column" colspan="2" attrs="{'invisible': [('debit_credit_column', '=', False)]}">
<field name="debit_value" attrs="{'required': [('debit_credit_column', '!=', False)]}"/>
<field name="credit_value" attrs="{'required': [('debit_credit_column', '!=', False)]}"/>
</group>
</group>
</xpath>
<xpath expr="//div[@name='final_buttons']/button" position="attributes">
<attribute name="class">btn-default</attribute>
<attribute name="string">Cancel</attribute>
</xpath>
<xpath expr="//div[@name='final_buttons']/button" position="before">
<button name="import_mapping" string="Import" type="object" class="btn-primary" />
</xpath>
</field>
</record>
<act_window
id="action_account_bank_statement_import_sheet_mapping_wizard"
name="Import Mapping"
res_model="account.bank.statement.import.sheet.mapping.wizard"
src_model="account.bank.statement.import.sheet.mapping"
view_mode="form"
target="new"
key2="client_action_multi"
/>
</odoo>

288
account_bank_statement_import_txt_xlsx/wizards/account_bank_statement_import_txt.py

@ -1,288 +0,0 @@
# Copyright 2014-2017 Akretion (http://www.akretion.com).
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# Copyright 2019 Eficent Business and IT Consulting Services, S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import xlrd
import logging
import datetime as dtm
from datetime import datetime
from odoo import _, api, fields, models
from odoo.exceptions import UserError
import re
from io import StringIO
_logger = logging.getLogger(__name__)
try:
import csv
except (ImportError, IOError) as err:
_logger.debug(err)
class AccountBankStatementImport(models.TransientModel):
_inherit = 'account.bank.statement.import'
txt_map_id = fields.Many2one(
comodel_name='account.bank.statement.import.map',
string='Txt map',
readonly=True,
)
@api.model
def _get_txt_encoding(self):
if self.txt_map_id.file_encoding:
return self.txt_map_id.file_encoding
return 'utf-8-sig'
@api.model
def _get_txt_str_data(self, data_file):
if not isinstance(data_file, str):
data_file = data_file.decode(self._get_txt_encoding())
return data_file.strip()
@api.model
def _txt_convert_amount(self, amount_str):
if not amount_str:
return 0.0
if self.txt_map_id:
thousands, decimal = self.txt_map_id._get_separators()
else:
thousands, decimal = ',', '.'
valstr = re.sub(r'[^\d%s%s.-]' % (thousands, decimal), '', amount_str)
valstrdot = valstr.replace(thousands, '')
valstrdot = valstrdot.replace(decimal, '.')
return float(valstrdot)
@api.model
def _check_xls(self, data_file):
# Try if it is an Excel file
headers = self.mapped('txt_map_id.map_line_ids.name')
try:
file_headers = []
book = xlrd.open_workbook(file_contents=data_file)
xl_sheet = book.sheet_by_index(0)
row = xl_sheet.row(0) # 1st row
for idx, cell_obj in enumerate(row):
cell_type_str = xlrd.sheet.ctype_text.get(cell_obj.ctype, False)
if cell_type_str:
file_headers.append(cell_obj.value)
else:
return False
if any(item not in file_headers for item in headers):
raise UserError(
_("Headers of file to import and Txt map lines does not "
"match."))
except xlrd.XLRDError as e:
return False
except Exception as e:
return False
return True
@api.model
def _check_txt(self, data_file):
data_file = self._get_txt_str_data(data_file)
if not self.txt_map_id:
return False
headers = self.mapped('txt_map_id.map_line_ids.name')
file_headers = data_file.split('\n', 1)[0]
if any(item not in file_headers for item in headers):
raise UserError(
_("Headers of file to import and Txt map lines does not "
"match."))
return True
def _get_currency_fields(self):
return ['amount', 'amount_currency']
def _convert_txt_line_to_dict(self, idx, line):
rline = dict()
for item in range(len(line)):
txt_map = self.mapped('txt_map_id.map_line_ids')[item]
value = line[item]
if not txt_map.field_to_assign:
continue
if txt_map.date_format:
try:
value = fields.Date.to_string(
datetime.strptime(value, txt_map.date_format))
except Exception:
raise UserError(
_("Date format of map file and Txt date does "
"not match."))
rline[txt_map.field_to_assign] = value
for field in self._get_currency_fields():
_logger.debug('Trying to convert %s to float' % rline[field])
try:
rline[field] = self._txt_convert_amount(rline[field])
except Exception:
raise UserError(
_("Value '%s' for the field '%s' on line %d, "
"cannot be converted to float")
% (rline[field], field, idx))
return rline
def _parse_txt_file(self, data_file):
data_file = self._get_txt_str_data(data_file)
f = StringIO(data_file)
f.seek(0)
raw_lines = []
if not self.txt_map_id.quotechar:
reader = csv.reader(f,
delimiter=self.txt_map_id.delimiter or False)
else:
reader = csv.reader(f,
quotechar=self.txt_map_id.quotechar,
delimiter=self.txt_map_id.delimiter or False)
next(reader) # Drop header
for idx, line in enumerate(reader):
_logger.debug("Line %d: %s" % (idx, line))
raw_lines.append(self._convert_txt_line_to_dict(idx, line))
return raw_lines
def _convert_xls_line_to_dict(self, row_idx, xl_sheet):
rline = dict()
for col_idx in range(0, xl_sheet.ncols): # Iterate through columns
txt_map = self.mapped('txt_map_id.map_line_ids')[col_idx]
cell_obj = xl_sheet.cell(row_idx, col_idx) # Get cell
ctype = xl_sheet.cell(row_idx, col_idx).ctype
value = cell_obj.value
if not txt_map.field_to_assign:
continue
if ctype == xlrd.XL_CELL_DATE:
ms_date_number = xl_sheet.cell(row_idx, col_idx).value
try:
year, month, day, hour, minute, \
second = xlrd.xldate_as_tuple(
ms_date_number, 0)
except xlrd.XLDateError as e:
raise UserError(
_('An error was found translating a date '
'field from the file: %s') % e)
value = dtm.date(year, month, day)
value = value.strftime('%Y-%m-%d')
rline[txt_map.field_to_assign] = value
return rline
def _parse_xls_file(self, data_file):
try:
raw_lines = []
book = xlrd.open_workbook(file_contents=data_file)
xl_sheet = book.sheet_by_index(0)
for row_idx in range(1, xl_sheet.nrows):
_logger.debug("Line %d" % row_idx)
raw_lines.append(self._convert_xls_line_to_dict(
row_idx, xl_sheet))
except xlrd.XLRDError:
return False
except Exception as e:
return False
return raw_lines
def _post_process_statement_line(self, raw_lines):
""" Enter your additional logic here. """
return raw_lines
def _get_journal(self):
journal_id = self.env.context.get('journal_id')
if not journal_id:
raise UserError(_('You must run this wizard from the journal'))
return self.env['account.journal'].browse(journal_id)
def _get_currency_id(self, fline):
journal = self._get_journal()
line_currency_name = fline.get('currency', False)
currency = journal.currency_id or journal.company_id.currency_id
if line_currency_name and line_currency_name != currency.name:
currency = self.env['res.currency'].search(
[('name', '=', fline['currency'])], limit=1)
return currency.id
return False
@api.model
def _get_partner_id(self, fline):
partner_name = fline.get('partner_name', False)
if partner_name:
partner = self.env['res.partner'].search([
('name', '=ilike', partner_name)])
if partner and len(partner) == 1:
return partner.commercial_partner_id.id
return None
def _prepare_txt_statement_line(self, fline):
currency_id = self._get_currency_id(fline)
return {
'date': fline.get('date', False),
'name': fline.get('name', ''),
'ref': fline.get('ref', False),
'note': fline.get('Notes', False),
'amount': fline.get('amount', 0.0),
'currency_id': self._get_currency_id(fline),
'amount_currency': currency_id and fline.get(
'amount_currency', 0.0) or 0.0,
'partner_id': self._get_partner_id(fline),
'account_number': fline.get('account_number', False),
}
def _prepare_txt_statement(self, lines):
balance_end_real = 0.0
for line in lines:
if 'amount' in line and line['amount']:
balance_end_real += line['amount']
return {
'name':
_('%s Import %s > %s')
% (self.txt_map_id.name,
lines[0]['date'], lines[-1]['date']),
'date': lines[-1]['date'],
'balance_start': 0.0,
'balance_end_real': balance_end_real,
}
@api.model
def _parse_file(self, data_file):
""" Import a file in Txt CSV format """
is_txt = False
is_xls = self._check_xls(data_file)
if not is_xls:
is_txt = self._check_txt(data_file)
if not is_txt and not is_xls:
return super(AccountBankStatementImport, self)._parse_file(
data_file)
if is_txt:
raw_lines = self._parse_txt_file(data_file)
else:
raw_lines = self._parse_xls_file(data_file)
final_lines = self._post_process_statement_line(raw_lines)
vals_bank_statement = self._prepare_txt_statement(final_lines)
transactions = []
for fline in final_lines:
vals_line = self._prepare_txt_statement_line(fline)
_logger.debug("vals_line = %s" % vals_line)
transactions.append(vals_line)
vals_bank_statement['transactions'] = transactions
return None, None, [vals_bank_statement]
@api.model
def _complete_txt_statement_line(self, line):
""" Enter additional logic here. """
return None
@api.model
def _complete_stmts_vals(self, stmts_vals, journal_id, account_number):
stmts_vals = super(AccountBankStatementImport, self). \
_complete_stmts_vals(stmts_vals, journal_id, account_number)
for line in stmts_vals[0]['transactions']:
vals = self._complete_txt_statement_line(line)
if vals:
line.update(vals)
return stmts_vals
@api.model
def default_get(self, fields):
res = super(AccountBankStatementImport, self).default_get(fields)
journal = self._get_journal()
res['txt_map_id'] = journal.statement_import_txt_map_id.id
return res

40
account_bank_statement_import_txt_xlsx/wizards/create_map_lines_from_file.py

@ -1,40 +0,0 @@
# Copyright 2019 Tecnativa - Vicent Cubells
# Copyright 2019 Eficent Business and IT Consulting Services, S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import csv
import base64
from odoo import api, fields, models
from io import StringIO
class WizardTxtMapCreate(models.TransientModel):
_name = 'wizard.txt.map.create'
_description = 'Wizard Txt Map Create'
data_file = fields.Binary(
string='Bank Statement File',
required=True,
)
filename = fields.Char()
@api.multi
def create_map_lines(self):
statement_obj = self.env['account.bank.statement.import.map']
data_file = base64.b64decode(self.data_file)
if not isinstance(data_file, str):
data_file = data_file.decode('utf-8-sig').strip()
file = StringIO(data_file)
file.seek(0)
reader = csv.reader(file)
headers = []
for row in reader:
headers = row
break
lines = []
for idx, title in enumerate(headers):
lines.append((0, 0, {'sequence': idx, 'name': title}))
if lines:
for statement in statement_obj.browse(
self.env.context.get('active_ids')):
statement.map_line_ids = lines

29
account_bank_statement_import_txt_xlsx/wizards/create_map_lines_from_file_views.xml

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="create_txt_map_lines_view" model="ir.ui.view">
<field name="name">Create Statement Map Lines</field>
<field name="model">wizard.txt.map.create</field>
<field name="arch" type="xml">
<form string="Import Txt Map Lines">
<h2>Select a TXT/CSV or XLSX bank statement file to create all the map lines from headers file.</h2>
<p>Download a bank statement from your bank and import it here.</p>
<p>All the txt map lines will be created automatically.</p>
<field name="data_file" filename="filename" placeholder="Choose a file to import..."/>
<field name="filename" invisible="1"/>
<footer>
<button name="create_map_lines" string="Create Lines" type="object" class="btn-primary" />
<button string="Cancel" class="btn-default" special="cancel"/>
</footer>
</form>
</field>
</record>
<act_window name="Create Statement Map Lines"
res_model="wizard.txt.map.create"
src_model="account.bank.statement.import.map"
view_mode="form"
target="new"
key2="client_action_multi"
id="action_create_txt_map_lines"/>
</odoo>
Loading…
Cancel
Save