Skip to content

Commit 7b8477f

Browse files
committed
[FIX] account_statement_import_sheet_file
Allow to import csv files with sheet mapping being configured with no_header and some header_lines_skip_count
1 parent 2bde977 commit 7b8477f

File tree

6 files changed

+192
-39
lines changed

6 files changed

+192
-39
lines changed

account_statement_import_sheet_file/models/account_statement_import_sheet_mapping.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,16 @@ class AccountStatementImportSheetMapping(models.Model):
163163
)
164164
footer_lines_skip_count = fields.Integer(
165165
string="Footer lines skip count",
166-
help="Set the Footer lines number."
167-
"Used in some csv/xlsx file that integrate meta data in"
168-
"last lines.",
166+
help="Define the number of footer lines to be skipped in CSV/XLSX"
167+
"files containing metadata at the bottom.",
169168
default="0",
170169
)
171170
header_lines_skip_count = fields.Integer(
172171
string="Header lines skip count",
173-
help="Set the Header lines number.",
172+
help="Define the number of header lines to be skipped in CSV/XLSX"
173+
"files containing metadata at the top."
174+
"The header line containing the titles of columns should "
175+
"be counted",
174176
default="0",
175177
)
176178
skip_empty_lines = fields.Boolean(

account_statement_import_sheet_file/models/account_statement_import_sheet_parser.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,16 @@ class AccountStatementImportSheetParser(models.TransientModel):
3939
_description = "Bank Statement Import Sheet Parser"
4040

4141
@api.model
42-
def parse_header(self, csv_or_xlsx, mapping):
42+
def parse_header(self, csv_or_xlsx, mapping, data_file_is_xlsx):
4343
if mapping.no_header:
4444
return []
4545
header_line = mapping.header_lines_skip_count
4646
# prevent negative indexes
4747
if header_line > 0:
4848
header_line -= 1
49-
if isinstance(csv_or_xlsx, tuple):
49+
if data_file_is_xlsx:
5050
header = [
51-
str(value).strip() for value in csv_or_xlsx[1].row_values(header_line)
51+
str(value).strip() for value in csv_or_xlsx.row_values(header_line)
5252
]
5353
else:
5454
[next(csv_or_xlsx) for _i in range(header_line)]
@@ -149,6 +149,7 @@ def _get_column_names(self):
149149
def _parse_lines(self, mapping, data_file, currency_code):
150150
columns = dict()
151151
try:
152+
data_file_is_xlsx = True
152153
workbook = xlrd.open_workbook(
153154
file_contents=data_file,
154155
encoding_override=(
@@ -160,6 +161,7 @@ def _parse_lines(self, mapping, data_file, currency_code):
160161
workbook.sheet_by_index(0),
161162
)
162163
except xlrd.XLRDError:
164+
data_file_is_xlsx = False
163165
csv_options = {}
164166
csv_delimiter = mapping._get_column_delimiter_character()
165167
if csv_delimiter:
@@ -176,8 +178,13 @@ def _parse_lines(self, mapping, data_file, currency_code):
176178
_("No valid encoding was found for the attached file")
177179
) from None
178180
decoded_file = data_file.decode(detected_encoding)
179-
csv_or_xlsx = reader(StringIO(decoded_file), **csv_options)
180-
header = self.parse_header(csv_or_xlsx, mapping)
181+
csv = reader(StringIO(decoded_file), **csv_options)
182+
csv_or_xlsx, csv_iterator_copy = itertools.tee(csv)
183+
184+
if data_file_is_xlsx:
185+
header = self.parse_header(workbook.sheet_by_index(0), mapping, data_file_is_xlsx)
186+
else:
187+
header = self.parse_header(csv_iterator_copy, mapping, data_file_is_xlsx)
181188

182189
# NOTE no seria necesario debit_column y credit_column ya que tenemos los
183190
# respectivos campos related
@@ -212,16 +219,17 @@ def _parse_rows(self, mapping, currency_code, data, columns): # noqa: C901
212219
else:
213220
numrows = len(str(data_file.strip()).split("\\n"))
214221

215-
label_line = mapping.header_lines_skip_count
216-
footer_line = numrows - mapping.footer_lines_skip_count
222+
data_first_index_line = mapping.header_lines_skip_count
223+
data_last_line = numrows - mapping.footer_lines_skip_count
224+
data_line_count = data_last_line - data_first_index_line
217225

218226
if isinstance(csv_or_xlsx, tuple):
219-
rows = range(label_line, footer_line)
227+
rows = range(data_first_index_line, data_last_line)
220228
else:
221229
rows = csv_or_xlsx
222230

223231
lines = []
224-
for index, row in enumerate(rows, label_line):
232+
for index, row in enumerate(rows):
225233
if isinstance(csv_or_xlsx, tuple):
226234
book = csv_or_xlsx[0]
227235
sheet = csv_or_xlsx[1]
@@ -233,7 +241,9 @@ def _parse_rows(self, mapping, currency_code, data, columns): # noqa: C901
233241
cell_value = xldate_as_datetime(cell_value, book.datemode)
234242
values.append(cell_value)
235243
else:
236-
if index >= footer_line:
244+
if index < data_first_index_line:
245+
continue
246+
if index >= data_first_index_line + data_line_count:
237247
continue
238248
values = list(row)
239249
if mapping.skip_empty_lines and not any(values):
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Bank code : 1001010101,Agency Code : 10000,Download start date : 01/04/2020,Download end date : 02/04/2020,,
2+
Account Number : 08088804068,Account Name : Account Owner,: EUR,,,
3+
,,,,,
4+
Balance at end of period,,,,"+31070,11",
5+
01/04/20,UNIQUE OP 1,LABEL 1,"-50,00",,DETAILS 1
6+
01/04/20,UNIQUE OP 2,LABEL 2,"-100,00",,CLIENTS X
7+
02/04/20,UNIQUE OP 3,LABEL 3,"-80,68",,DETAILS 2
8+
02/04/20,UNIQUE OP 4,LABEL 4,,"1300,00",DETAILS 3
9+
Balance at start of period,,,,"+30000,77",
Binary file not shown.

account_statement_import_sheet_file/tests/test_account_statement_import_sheet_file.py

Lines changed: 157 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -204,13 +204,12 @@ def test_original_currency(self):
204204
line_amount_currency = float_round(line.amount_currency, precision_digits=1)
205205
self.assertEqual(line_amount_currency, 1000.0)
206206

207-
def test_original_currency_no_header(self):
207+
def test_original_currency_no_header_csv(self):
208208
no_header_statement_map = self.AccountStatementImportSheetMapping.create(
209209
{
210210
"name": "Sample Statement",
211211
"float_thousands_sep": "comma",
212212
"float_decimal_sep": "dot",
213-
"header_lines_skip_count": 0,
214213
"delimiter": "comma",
215214
"quotechar": '"',
216215
"timestamp_format": "%m/%d/%Y",
@@ -236,7 +235,57 @@ def test_original_currency_no_header(self):
236235
data = self._data_file("fixtures/original_currency_no_header.csv", "utf-8")
237236
wizard = self.AccountStatementImport.with_context(journal_id=journal.id).create(
238237
{
239-
"statement_filename": "fixtures/original_currency.csv",
238+
"statement_filename": "fixtures/original_currency_no_header.csv",
239+
"statement_file": data,
240+
"sheet_mapping_id": no_header_statement_map.id,
241+
}
242+
)
243+
wizard.with_context(
244+
account_statement_import_sheet_file_test=True
245+
).import_file_button()
246+
statement = self.AccountBankStatement.search([("journal_id", "=", journal.id)])
247+
self.assertEqual(len(statement), 1)
248+
self.assertEqual(len(statement.line_ids), 1)
249+
250+
line = statement.line_ids
251+
self.assertEqual(line.currency_id, self.currency_usd)
252+
self.assertEqual(line.foreign_currency_id, self.currency_eur)
253+
self.assertEqual(line.amount_currency, 1000.0)
254+
self.assertEqual(line.payment_ref, "Your payment INV0001")
255+
256+
def test_original_currency_no_header_xlsx(self):
257+
no_header_statement_map = self.AccountStatementImportSheetMapping.create(
258+
{
259+
"name": "Sample Statement",
260+
"float_thousands_sep": "comma",
261+
"float_decimal_sep": "dot",
262+
"delimiter": "comma",
263+
"quotechar": '"',
264+
"timestamp_format": "%m/%d/%Y",
265+
"no_header": True,
266+
"timestamp_column": "0",
267+
"amount_column": "3",
268+
"original_currency_column": "2",
269+
"original_amount_column": "4",
270+
"description_column": "1,7",
271+
"partner_name_column": "5",
272+
"bank_account_column": "6",
273+
}
274+
)
275+
journal = self.AccountJournal.create(
276+
{
277+
"name": "Bank",
278+
"type": "bank",
279+
"code": "BANK",
280+
"currency_id": self.currency_usd.id,
281+
"suspense_account_id": self.suspense_account.id,
282+
}
283+
)
284+
285+
data = self._data_file("fixtures/original_currency_no_header.xlsx")
286+
wizard = self.AccountStatementImport.with_context(journal_id=journal.id).create(
287+
{
288+
"statement_filename": "fixtures/original_currency_no_header.xlsx",
240289
"statement_file": data,
241290
"sheet_mapping_id": no_header_statement_map.id,
242291
}
@@ -478,6 +527,58 @@ def test_metadata_separated_debit_credit_csv(self):
478527
self.assertEqual(line1.amount, 50)
479528
self.assertEqual(line4.amount, -1300)
480529

530+
def test_metadata_separated_debit_credit_no_header_csv(self):
531+
journal = self.AccountJournal.create(
532+
{
533+
"name": "Bank",
534+
"type": "bank",
535+
"code": "BANK",
536+
"currency_id": self.currency_usd.id,
537+
"suspense_account_id": self.suspense_account.id,
538+
}
539+
)
540+
statement_map = self.sample_statement_map.copy(
541+
{
542+
"footer_lines_skip_count": 1,
543+
"header_lines_skip_count": 4,
544+
"no_header": True,
545+
"amount_column": None,
546+
"partner_name_column": None,
547+
"bank_account_column": None,
548+
"float_thousands_sep": "none",
549+
"float_decimal_sep": "comma",
550+
"timestamp_format": "%m/%d/%y",
551+
"timestamp_column": "0",
552+
"description_column": "2",
553+
"original_currency_column": None,
554+
"original_amount_column": None,
555+
"amount_type": "distinct_credit_debit",
556+
"amount_debit_column": "3",
557+
"amount_credit_column": "4",
558+
}
559+
)
560+
data = self._data_file(
561+
"fixtures/meta_data_separated_credit_debit_no_header.csv", "utf-8"
562+
)
563+
wizard = self.AccountStatementImport.with_context(journal_id=journal.id).create(
564+
{
565+
"statement_filename": "fixtures/meta_data_separated_credit_debit_no_header.csv",
566+
"statement_file": data,
567+
"sheet_mapping_id": statement_map.id,
568+
}
569+
)
570+
wizard.with_context(
571+
journal_id=journal.id,
572+
account_bank_statement_import_txt_xlsx_test=True,
573+
).import_file_button()
574+
statement = self.AccountBankStatement.search([("journal_id", "=", journal.id)])
575+
self.assertEqual(len(statement), 1)
576+
self.assertEqual(len(statement.line_ids), 4)
577+
line1 = statement.line_ids.filtered(lambda x: x.payment_ref == "LABEL 1")
578+
line4 = statement.line_ids.filtered(lambda x: x.payment_ref == "LABEL 4")
579+
self.assertEqual(line1.amount, 50)
580+
self.assertEqual(line4.amount, -1300)
581+
481582
def test_metadata_separated_debit_credit_xlsx(self):
482583
journal = self.AccountJournal.create(
483584
{
@@ -525,6 +626,59 @@ def test_metadata_separated_debit_credit_xlsx(self):
525626
self.assertEqual(line1.amount, 50)
526627
self.assertEqual(line4.amount, -1300)
527628

629+
def test_metadata_separated_debit_credit_no_header_xlsx(self):
630+
journal = self.AccountJournal.create(
631+
{
632+
"name": "Bank",
633+
"type": "bank",
634+
"code": "BANK",
635+
"currency_id": self.currency_usd.id,
636+
"suspense_account_id": self.suspense_account.id,
637+
}
638+
)
639+
statement_map = self.sample_statement_map.copy(
640+
{
641+
"footer_lines_skip_count": 1,
642+
"header_lines_skip_count": 4,
643+
"no_header": True,
644+
"amount_column": None,
645+
"partner_name_column": None,
646+
"bank_account_column": None,
647+
"float_thousands_sep": "none",
648+
"float_decimal_sep": "comma",
649+
"timestamp_format": "%m/%d/%y",
650+
"timestamp_column": "0",
651+
"description_column": "2",
652+
"original_currency_column": None,
653+
"original_amount_column": None,
654+
"amount_type": "distinct_credit_debit",
655+
"amount_debit_column": "3",
656+
"amount_credit_column": "4",
657+
}
658+
)
659+
filename = "fixtures/meta_data_separated_credit_debit_no_header.xlsx"
660+
data = self._data_file(
661+
"fixtures/meta_data_separated_credit_debit_no_header.xlsx"
662+
)
663+
wizard = self.AccountStatementImport.with_context(journal_id=journal.id).create(
664+
{
665+
"statement_filename": filename,
666+
"statement_file": data,
667+
"sheet_mapping_id": statement_map.id,
668+
}
669+
)
670+
wizard.with_context(
671+
journal_id=journal.id,
672+
account_bank_statement_import_txt_xlsx_test=True,
673+
).import_file_button()
674+
statement = self.AccountBankStatement.search([("journal_id", "=", journal.id)])
675+
self.assertEqual(len(statement), 1)
676+
self.assertEqual(len(statement.line_ids), 4)
677+
line1 = statement.line_ids.filtered(lambda x: x.payment_ref == "LABEL 1")
678+
line4 = statement.line_ids.filtered(lambda x: x.payment_ref == "LABEL 4")
679+
self.assertEqual(line1.amount, 50)
680+
self.assertEqual(line4.amount, -1300)
681+
528682
def test_amount_inverse_sign(self):
529683
self.sample_statement_map.amount_inverse_sign = True
530684
journal = self.AccountJournal.create(
@@ -701,17 +855,6 @@ def test_offsets(self):
701855
)
702856
file_name = "fixtures/sample_statement_offsets.xlsx"
703857
data = self._data_file(file_name)
704-
wizard = self.AccountStatementImport.with_context(journal_id=journal.id).create(
705-
{
706-
"statement_filename": file_name,
707-
"statement_file": data,
708-
"sheet_mapping_id": self.sample_statement_map.id,
709-
}
710-
)
711-
with self.assertRaises(UserError):
712-
wizard.with_context(
713-
account_statement_import_txt_xlsx_test=True
714-
).import_file_button()
715858
statement_map_offsets = self.sample_statement_map.copy(
716859
{
717860
"offset_column": 1,
@@ -748,17 +891,6 @@ def test_skip_empty_lines(self):
748891
file_name = "fixtures/empty_lines_statement.csv"
749892
data = self._data_file(file_name, "utf-8")
750893

751-
wizard = self.AccountStatementImport.with_context(journal_id=journal.id).create(
752-
{
753-
"statement_filename": file_name,
754-
"statement_file": data,
755-
"sheet_mapping_id": self.sample_statement_map.id,
756-
}
757-
)
758-
with self.assertRaises(UserError):
759-
wizard.with_context(
760-
account_statement_import_txt_xlsx_test=True
761-
).import_file_button()
762894
statement_map_empty_line = self.sample_statement_map.copy(
763895
{
764896
"skip_empty_lines": True,

0 commit comments

Comments
 (0)