mirror of
https://github.com/OCA/bank-statement-import.git
synced 2025-01-20 12:37:43 +02:00
@@ -103,6 +103,8 @@ Contributors
|
|||||||
|
|
||||||
* Alexey Pelykh <alexey.pelykh@corphub.eu>
|
* Alexey Pelykh <alexey.pelykh@corphub.eu>
|
||||||
|
|
||||||
|
* Sebastiano Picchi <sebastiano.picchi@pytech.it>
|
||||||
|
|
||||||
Maintainers
|
Maintainers
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,14 @@ class AccountStatementImportSheetMapping(models.Model):
|
|||||||
help="Set the Header lines number.",
|
help="Set the Header lines number.",
|
||||||
default="0",
|
default="0",
|
||||||
)
|
)
|
||||||
|
skip_empty_lines = fields.Boolean(
|
||||||
|
default=False,
|
||||||
|
help="Allows to skip empty lines",
|
||||||
|
)
|
||||||
|
offset_column = fields.Integer(
|
||||||
|
default=0,
|
||||||
|
help="Horizontal spaces to ignore before starting to parse",
|
||||||
|
)
|
||||||
|
|
||||||
@api.constrains(
|
@api.constrains(
|
||||||
"amount_type",
|
"amount_type",
|
||||||
@@ -217,6 +225,12 @@ class AccountStatementImportSheetMapping(models.Model):
|
|||||||
elif "comma" == self.float_thousands_sep == self.float_decimal_sep:
|
elif "comma" == self.float_thousands_sep == self.float_decimal_sep:
|
||||||
self.float_thousands_sep = "dot"
|
self.float_thousands_sep = "dot"
|
||||||
|
|
||||||
|
@api.constrains("offset_column")
|
||||||
|
def _check_columns(self):
|
||||||
|
for mapping in self:
|
||||||
|
if mapping.offset_column < 0:
|
||||||
|
raise ValidationError(_("Offsets cannot be negative"))
|
||||||
|
|
||||||
def _get_float_separators(self):
|
def _get_float_separators(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
separators = {
|
separators = {
|
||||||
|
|||||||
@@ -188,6 +188,8 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
|||||||
else:
|
else:
|
||||||
[next(csv_or_xlsx) for _i in range(header_line)]
|
[next(csv_or_xlsx) for _i in range(header_line)]
|
||||||
header = [value.strip() for value in next(csv_or_xlsx)]
|
header = [value.strip() for value in next(csv_or_xlsx)]
|
||||||
|
if mapping.offset_column:
|
||||||
|
header = header[mapping.offset_column :]
|
||||||
|
|
||||||
# NOTE no seria necesario debit_column y credit_column ya que tenemos los
|
# NOTE no seria necesario debit_column y credit_column ya que tenemos los
|
||||||
# respectivos campos related
|
# respectivos campos related
|
||||||
@@ -226,7 +228,7 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
|||||||
footer_line = numrows - mapping.footer_lines_skip_count
|
footer_line = numrows - mapping.footer_lines_skip_count
|
||||||
|
|
||||||
if isinstance(csv_or_xlsx, tuple):
|
if isinstance(csv_or_xlsx, tuple):
|
||||||
rows = range(mapping.header_lines_skip_count, footer_line)
|
rows = range(label_line, footer_line)
|
||||||
else:
|
else:
|
||||||
rows = csv_or_xlsx
|
rows = csv_or_xlsx
|
||||||
|
|
||||||
@@ -236,7 +238,7 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
|||||||
book = csv_or_xlsx[0]
|
book = csv_or_xlsx[0]
|
||||||
sheet = csv_or_xlsx[1]
|
sheet = csv_or_xlsx[1]
|
||||||
values = []
|
values = []
|
||||||
for col_index in range(0, sheet.row_len(row)):
|
for col_index in range(mapping.offset_column, sheet.row_len(row)):
|
||||||
cell_type = sheet.cell_type(row, col_index)
|
cell_type = sheet.cell_type(row, col_index)
|
||||||
cell_value = sheet.cell_value(row, col_index)
|
cell_value = sheet.cell_value(row, col_index)
|
||||||
if cell_type == xlrd.XL_CELL_DATE:
|
if cell_type == xlrd.XL_CELL_DATE:
|
||||||
@@ -246,6 +248,8 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
|||||||
if index >= footer_line:
|
if index >= footer_line:
|
||||||
continue
|
continue
|
||||||
values = list(row)
|
values = list(row)
|
||||||
|
if mapping.skip_empty_lines and not any(values):
|
||||||
|
continue
|
||||||
|
|
||||||
timestamp = self._get_values_from_column(
|
timestamp = self._get_values_from_column(
|
||||||
values, columns, "timestamp_column"
|
values, columns, "timestamp_column"
|
||||||
@@ -375,7 +379,9 @@ class AccountStatementImportSheetParser(models.TransientModel):
|
|||||||
line["bank_name"] = bank_name
|
line["bank_name"] = bank_name
|
||||||
if bank_account is not None:
|
if bank_account is not None:
|
||||||
line["bank_account"] = bank_account
|
line["bank_account"] = bank_account
|
||||||
lines.append(line)
|
|
||||||
|
if line:
|
||||||
|
lines.append(line)
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
|
|||||||
@@ -15,3 +15,5 @@
|
|||||||
* `CorporateHub <https://corporatehub.eu/>`__
|
* `CorporateHub <https://corporatehub.eu/>`__
|
||||||
|
|
||||||
* Alexey Pelykh <alexey.pelykh@corphub.eu>
|
* Alexey Pelykh <alexey.pelykh@corphub.eu>
|
||||||
|
|
||||||
|
* Sebastiano Picchi <sebastiano.picchi@pytech.it>
|
||||||
|
|||||||
@@ -8,10 +8,11 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
:Author: David Goodger (goodger@python.org)
|
:Author: David Goodger (goodger@python.org)
|
||||||
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
|
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
|
||||||
:Copyright: This stylesheet has been placed in the public domain.
|
:Copyright: This stylesheet has been placed in the public domain.
|
||||||
|
|
||||||
Default cascading style sheet for the HTML output of Docutils.
|
Default cascading style sheet for the HTML output of Docutils.
|
||||||
|
Despite the name, some widely supported CSS2 features are used.
|
||||||
|
|
||||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||||
customize this style sheet.
|
customize this style sheet.
|
||||||
@@ -274,7 +275,7 @@ pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
|||||||
margin-left: 2em ;
|
margin-left: 2em ;
|
||||||
margin-right: 2em }
|
margin-right: 2em }
|
||||||
|
|
||||||
pre.code .ln { color: grey; } /* line numbers */
|
pre.code .ln { color: gray; } /* line numbers */
|
||||||
pre.code, code { background-color: #eeeeee }
|
pre.code, code { background-color: #eeeeee }
|
||||||
pre.code .comment, code .comment { color: #5C6576 }
|
pre.code .comment, code .comment { color: #5C6576 }
|
||||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||||
@@ -300,7 +301,7 @@ span.option {
|
|||||||
span.pre {
|
span.pre {
|
||||||
white-space: pre }
|
white-space: pre }
|
||||||
|
|
||||||
span.problematic {
|
span.problematic, pre.problematic {
|
||||||
color: red }
|
color: red }
|
||||||
|
|
||||||
span.section-subtitle {
|
span.section-subtitle {
|
||||||
@@ -455,12 +456,15 @@ If you spotted it first, help us to smash it by providing a detailed and welcome
|
|||||||
<li>Alexey Pelykh <<a class="reference external" href="mailto:alexey.pelykh@corphub.eu">alexey.pelykh@corphub.eu</a>></li>
|
<li>Alexey Pelykh <<a class="reference external" href="mailto:alexey.pelykh@corphub.eu">alexey.pelykh@corphub.eu</a>></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
<li>Sebastiano Picchi <<a class="reference external" href="mailto:sebastiano.picchi@pytech.it">sebastiano.picchi@pytech.it</a>></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" id="maintainers">
|
<div class="section" id="maintainers">
|
||||||
<h2><a class="toc-backref" href="#toc-entry-9">Maintainers</a></h2>
|
<h2><a class="toc-backref" href="#toc-entry-9">Maintainers</a></h2>
|
||||||
<p>This module is maintained by the OCA.</p>
|
<p>This module is maintained by the OCA.</p>
|
||||||
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
|
<a class="reference external image-reference" href="https://odoo-community.org">
|
||||||
|
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
|
||||||
|
</a>
|
||||||
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||||
mission is to support the collaborative development of Odoo features and
|
mission is to support the collaborative development of Odoo features and
|
||||||
promote its widespread use.</p>
|
promote its widespread use.</p>
|
||||||
|
|||||||
5
account_statement_import_sheet_file/tests/fixtures/empty_lines_statement.csv
vendored
Normal file
5
account_statement_import_sheet_file/tests/fixtures/empty_lines_statement.csv
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"Date","Label","Currency","Amount","Amount Currency","Partner Name","Bank Account"
|
||||||
|
"02/25/2018","AAAOOO 1","EUR","-33.50","0.0","John Doe","123456789"
|
||||||
|
"02/26/2018","AAAOOO 2","EUR","1,525.00","1,000.00","Azure Interior",""
|
||||||
|
,,,,,,
|
||||||
|
"02/27/2018","AAAOOO 3","EUR","800.00","800.00","Azure Interior","123456789"
|
||||||
|
BIN
account_statement_import_sheet_file/tests/fixtures/sample_statement_offsets.xlsx
vendored
Normal file
BIN
account_statement_import_sheet_file/tests/fixtures/sample_statement_offsets.xlsx
vendored
Normal file
Binary file not shown.
@@ -678,3 +678,95 @@ class TestAccountStatementImportSheetFile(common.TransactionCase):
|
|||||||
self.parser._parse_decimal(Decimal("1234.56"), self.mock_mapping_comma_dot),
|
self.parser._parse_decimal(Decimal("1234.56"), self.mock_mapping_comma_dot),
|
||||||
1234.56,
|
1234.56,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_offsets(self):
|
||||||
|
journal = self.AccountJournal.create(
|
||||||
|
{
|
||||||
|
"name": "Bank",
|
||||||
|
"type": "bank",
|
||||||
|
"code": "BANK",
|
||||||
|
"currency_id": self.currency_usd.id,
|
||||||
|
"suspense_account_id": self.suspense_account.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
file_name = "fixtures/sample_statement_offsets.xlsx"
|
||||||
|
data = self._data_file(file_name)
|
||||||
|
wizard = self.AccountStatementImport.with_context(journal_id=journal.id).create(
|
||||||
|
{
|
||||||
|
"statement_filename": file_name,
|
||||||
|
"statement_file": data,
|
||||||
|
"sheet_mapping_id": self.sample_statement_map.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
wizard.with_context(
|
||||||
|
account_statement_import_txt_xlsx_test=True
|
||||||
|
).import_file_button()
|
||||||
|
statement_map_offsets = self.sample_statement_map.copy(
|
||||||
|
{
|
||||||
|
"offset_column": 1,
|
||||||
|
"header_lines_skip_count": 3,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
wizard = self.AccountStatementImport.with_context(journal_id=journal.id).create(
|
||||||
|
{
|
||||||
|
"statement_filename": file_name,
|
||||||
|
"statement_file": data,
|
||||||
|
"sheet_mapping_id": statement_map_offsets.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
wizard.with_context(
|
||||||
|
account_statement_import_txt_xlsx_test=True
|
||||||
|
).import_file_button()
|
||||||
|
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, 0.0)
|
||||||
|
self.assertEqual(statement.balance_end_real, 1491.5)
|
||||||
|
self.assertEqual(statement.balance_end, 1491.5)
|
||||||
|
|
||||||
|
def test_skip_empty_lines(self):
|
||||||
|
journal = self.AccountJournal.create(
|
||||||
|
{
|
||||||
|
"name": "Bank",
|
||||||
|
"type": "bank",
|
||||||
|
"code": "BANK",
|
||||||
|
"currency_id": self.currency_usd.id,
|
||||||
|
"suspense_account_id": self.suspense_account.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
file_name = "fixtures/empty_lines_statement.csv"
|
||||||
|
data = self._data_file(file_name, "utf-8")
|
||||||
|
|
||||||
|
wizard = self.AccountStatementImport.with_context(journal_id=journal.id).create(
|
||||||
|
{
|
||||||
|
"statement_filename": file_name,
|
||||||
|
"statement_file": data,
|
||||||
|
"sheet_mapping_id": self.sample_statement_map.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
wizard.with_context(
|
||||||
|
account_statement_import_txt_xlsx_test=True
|
||||||
|
).import_file_button()
|
||||||
|
statement_map_empty_line = self.sample_statement_map.copy(
|
||||||
|
{
|
||||||
|
"skip_empty_lines": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
wizard = self.AccountStatementImport.with_context(journal_id=journal.id).create(
|
||||||
|
{
|
||||||
|
"statement_filename": file_name,
|
||||||
|
"statement_file": data,
|
||||||
|
"sheet_mapping_id": statement_map_empty_line.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
wizard.with_context(
|
||||||
|
account_statement_import_txt_xlsx_test=True
|
||||||
|
).import_file_button()
|
||||||
|
statement = self.AccountBankStatement.search([("journal_id", "=", journal.id)])
|
||||||
|
self.assertEqual(len(statement), 1)
|
||||||
|
self.assertEqual(len(statement.line_ids), 3)
|
||||||
|
self.assertEqual(statement.balance_start, 0.0)
|
||||||
|
self.assertEqual(statement.balance_end_real, 2291.5)
|
||||||
|
self.assertEqual(statement.balance_end, 2291.5)
|
||||||
|
|||||||
@@ -64,8 +64,13 @@
|
|||||||
/>
|
/>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
|
<field name="skip_empty_lines" />
|
||||||
<field name="header_lines_skip_count" />
|
<field name="header_lines_skip_count" />
|
||||||
<field name="footer_lines_skip_count" />
|
<field name="footer_lines_skip_count" />
|
||||||
|
<field
|
||||||
|
name="offset_column"
|
||||||
|
attrs="{'invisible': [('no_header', '=', True)]}"
|
||||||
|
/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<group string="Columns">
|
<group string="Columns">
|
||||||
|
|||||||
Reference in New Issue
Block a user