You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

338 lines
12 KiB

  1. # Copyright 2019 ForgeFlow, S.L.
  2. # Copyright 2020 CorporateHub (https://corporatehub.eu)
  3. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
  4. from odoo import api, models, _
  5. from datetime import datetime
  6. from decimal import Decimal
  7. from io import StringIO
  8. from os import path
  9. import itertools
  10. import logging
  11. _logger = logging.getLogger(__name__)
  12. try:
  13. from csv import reader
  14. import xlrd
  15. from xlrd.xldate import xldate_as_datetime
  16. except (ImportError, IOError) as err: # pragma: no cover
  17. _logger.error(err)
  18. class AccountBankStatementImportSheetParser(models.TransientModel):
  19. _name = 'account.bank.statement.import.sheet.parser'
  20. _description = 'Account Bank Statement Import Sheet Parser'
  21. @api.model
  22. def parse_header(self, data_file, encoding, csv_options):
  23. try:
  24. workbook = xlrd.open_workbook(
  25. file_contents=data_file,
  26. encoding_override=encoding if encoding else None,
  27. )
  28. sheet = workbook.sheet_by_index(0)
  29. values = sheet.row_values(0)
  30. return [str(value) for value in values]
  31. except xlrd.XLRDError:
  32. pass
  33. data = StringIO(data_file.decode(encoding or 'utf-8'))
  34. csv_data = reader(data, **csv_options)
  35. return list(next(csv_data))
  36. @api.model
  37. def parse(self, mapping, data_file, filename):
  38. journal = self.env['account.journal'].browse(
  39. self.env.context.get('journal_id')
  40. )
  41. currency_code = (
  42. journal.currency_id or journal.company_id.currency_id
  43. ).name
  44. account_number = journal.bank_account_id.acc_number
  45. name = _('%s: %s') % (
  46. journal.code,
  47. path.basename(filename),
  48. )
  49. lines = self._parse_lines(mapping, data_file, currency_code)
  50. if not lines:
  51. return currency_code, account_number, [{
  52. 'name': name,
  53. 'transactions': [],
  54. }]
  55. lines = list(sorted(
  56. lines,
  57. key=lambda line: line['timestamp']
  58. ))
  59. first_line = lines[0]
  60. last_line = lines[-1]
  61. data = {
  62. 'name': name,
  63. 'date': first_line['timestamp'].date(),
  64. }
  65. if mapping.balance_column:
  66. balance_start = first_line['balance']
  67. balance_start -= first_line['amount']
  68. balance_end = last_line['balance']
  69. data.update({
  70. 'balance_start': float(balance_start),
  71. 'balance_end_real': float(balance_end),
  72. })
  73. transactions = list(itertools.chain.from_iterable(map(
  74. lambda line: self._convert_line_to_transactions(line),
  75. lines
  76. )))
  77. data.update({
  78. 'transactions': transactions,
  79. })
  80. return currency_code, account_number, [data]
  81. def _parse_lines(self, mapping, data_file, currency_code):
  82. try:
  83. workbook = xlrd.open_workbook(
  84. file_contents=data_file,
  85. encoding_override=(
  86. mapping.file_encoding if mapping.file_encoding else None
  87. ),
  88. )
  89. csv_or_xlsx = (workbook, workbook.sheet_by_index(0),)
  90. except xlrd.XLRDError:
  91. csv_options = {}
  92. csv_delimiter = mapping._get_column_delimiter_character()
  93. if csv_delimiter:
  94. csv_options['delimiter'] = csv_delimiter
  95. if mapping.quotechar:
  96. csv_options['quotechar'] = mapping.quotechar
  97. csv_or_xlsx = reader(
  98. StringIO(data_file.decode(mapping.file_encoding or 'utf-8')),
  99. **csv_options
  100. )
  101. if isinstance(csv_or_xlsx, tuple):
  102. header = [str(value) for value in csv_or_xlsx[1].row_values(0)]
  103. else:
  104. header = [value.strip() for value in next(csv_or_xlsx)]
  105. timestamp_column = header.index(mapping.timestamp_column)
  106. currency_column = header.index(mapping.currency_column) \
  107. if mapping.currency_column else None
  108. amount_column = header.index(mapping.amount_column)
  109. balance_column = header.index(mapping.balance_column) \
  110. if mapping.balance_column else None
  111. original_currency_column = (
  112. header.index(mapping.original_currency_column)
  113. if mapping.original_currency_column else None
  114. )
  115. original_amount_column = (
  116. header.index(mapping.original_amount_column)
  117. if mapping.original_amount_column else None
  118. )
  119. debit_credit_column = header.index(mapping.debit_credit_column) \
  120. if mapping.debit_credit_column else None
  121. transaction_id_column = header.index(mapping.transaction_id_column) \
  122. if mapping.transaction_id_column else None
  123. description_column = header.index(mapping.description_column) \
  124. if mapping.description_column else None
  125. notes_column = header.index(mapping.notes_column) \
  126. if mapping.notes_column else None
  127. reference_column = header.index(mapping.reference_column) \
  128. if mapping.reference_column else None
  129. partner_name_column = header.index(mapping.partner_name_column) \
  130. if mapping.partner_name_column else None
  131. bank_name_column = header.index(mapping.bank_name_column) \
  132. if mapping.bank_name_column else None
  133. bank_account_column = header.index(mapping.bank_account_column) \
  134. if mapping.bank_account_column else None
  135. if isinstance(csv_or_xlsx, tuple):
  136. rows = range(1, csv_or_xlsx[1].nrows)
  137. else:
  138. rows = csv_or_xlsx
  139. lines = []
  140. for row in rows:
  141. if isinstance(csv_or_xlsx, tuple):
  142. book = csv_or_xlsx[0]
  143. sheet = csv_or_xlsx[1]
  144. values = []
  145. for col_index in range(sheet.row_len(row)):
  146. cell_type = sheet.cell_type(row, col_index)
  147. cell_value = sheet.cell_value(row, col_index)
  148. if cell_type == xlrd.XL_CELL_DATE:
  149. cell_value = xldate_as_datetime(cell_value, book.datemode)
  150. values.append(cell_value)
  151. else:
  152. values = list(row)
  153. timestamp = values[timestamp_column]
  154. currency = values[currency_column] \
  155. if currency_column is not None else currency_code
  156. amount = values[amount_column]
  157. balance = values[balance_column] \
  158. if balance_column is not None else None
  159. original_currency = values[original_currency_column] \
  160. if original_currency_column is not None else None
  161. original_amount = values[original_amount_column] \
  162. if original_amount_column is not None else None
  163. debit_credit = values[debit_credit_column] \
  164. if debit_credit_column is not None else None
  165. transaction_id = values[transaction_id_column] \
  166. if transaction_id_column is not None else None
  167. description = values[description_column] \
  168. if description_column is not None else None
  169. notes = values[notes_column] \
  170. if notes_column is not None else None
  171. reference = values[reference_column] \
  172. if reference_column is not None else None
  173. partner_name = values[partner_name_column] \
  174. if partner_name_column is not None else None
  175. bank_name = values[bank_name_column] \
  176. if bank_name_column is not None else None
  177. bank_account = values[bank_account_column] \
  178. if bank_account_column is not None else None
  179. if currency != currency_code:
  180. continue
  181. if isinstance(timestamp, str):
  182. timestamp = datetime.strptime(
  183. timestamp,
  184. mapping.timestamp_format
  185. )
  186. amount = self._parse_decimal(amount, mapping)
  187. if balance:
  188. balance = self._parse_decimal(balance, mapping)
  189. else:
  190. balance = None
  191. if debit_credit:
  192. amount = amount.copy_abs()
  193. if debit_credit == mapping.debit_value:
  194. amount = -amount
  195. if not original_currency:
  196. original_currency = currency
  197. original_amount = amount
  198. elif original_currency == currency:
  199. original_amount = amount
  200. if original_amount:
  201. original_amount = self._parse_decimal(
  202. original_amount,
  203. mapping
  204. ).copy_sign(amount)
  205. else:
  206. original_amount = 0.0
  207. line = {
  208. 'timestamp': timestamp,
  209. 'amount': amount,
  210. 'currency': currency,
  211. 'original_amount': original_amount,
  212. 'original_currency': original_currency,
  213. }
  214. if balance is not None:
  215. line['balance'] = balance
  216. if transaction_id is not None:
  217. line['transaction_id'] = transaction_id
  218. if description is not None:
  219. line['description'] = description
  220. if notes is not None:
  221. line['notes'] = notes
  222. if reference is not None:
  223. line['reference'] = reference
  224. if partner_name is not None:
  225. line['partner_name'] = partner_name
  226. if bank_name is not None:
  227. line['bank_name'] = bank_name
  228. if bank_account is not None:
  229. line['bank_account'] = bank_account
  230. lines.append(line)
  231. return lines
  232. @api.model
  233. def _convert_line_to_transactions(self, line):
  234. """Hook for extension"""
  235. timestamp = line['timestamp']
  236. amount = line['amount']
  237. currency = line['currency']
  238. original_amount = line['original_amount']
  239. original_currency = line['original_currency']
  240. transaction_id = line.get('transaction_id')
  241. description = line.get('description')
  242. notes = line.get('notes')
  243. reference = line.get('reference')
  244. partner_name = line.get('partner_name')
  245. bank_name = line.get('bank_name')
  246. bank_account = line.get('bank_account')
  247. transaction = {
  248. 'date': timestamp,
  249. 'amount': str(amount),
  250. }
  251. if currency != original_currency:
  252. original_currency = self.env['res.currency'].search(
  253. [('name', '=', original_currency)],
  254. limit=1,
  255. )
  256. if original_currency:
  257. transaction.update({
  258. 'amount_currency': str(original_amount),
  259. 'currency_id': original_currency.id,
  260. })
  261. if transaction_id:
  262. transaction['unique_import_id'] = '%s-%s' % (
  263. transaction_id,
  264. int(timestamp.timestamp()),
  265. )
  266. transaction['name'] = description or _('N/A')
  267. if reference:
  268. transaction['ref'] = reference
  269. note = ''
  270. if bank_name:
  271. note += _('Bank: %s; ') % (
  272. bank_name,
  273. )
  274. if bank_account:
  275. note += _('Account: %s; ') % (
  276. bank_account,
  277. )
  278. if transaction_id:
  279. note += _('Transaction ID: %s; ') % (
  280. transaction_id,
  281. )
  282. if note and notes:
  283. note = "{}\n{}".format(notes, note.strip())
  284. elif note:
  285. note = note.strip()
  286. elif notes:
  287. note = notes
  288. if note:
  289. transaction['note'] = note
  290. if partner_name:
  291. transaction['partner_name'] = partner_name
  292. if bank_account:
  293. transaction['account_number'] = bank_account
  294. return [transaction]
  295. @api.model
  296. def _parse_decimal(self, value, mapping):
  297. if isinstance(value, Decimal):
  298. return value
  299. elif isinstance(value, float):
  300. return Decimal(value)
  301. thousands, decimal = mapping._get_float_separators()
  302. value = value.replace(thousands, '')
  303. value = value.replace(decimal, '.')
  304. return Decimal(value)