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.

275 lines
9.9 KiB

  1. # Copyright 2019 Tecnativa - Vicent Cubells
  2. # Copyright 2019 Brainbean Apps (https://brainbeanapps.com)
  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. from pytz import timezone, utc
  11. import logging
  12. _logger = logging.getLogger(__name__)
  13. try:
  14. from csv import reader
  15. except (ImportError, IOError) as err:
  16. _logger.error(err)
  17. class AccountBankStatementImportPayPalParser(models.TransientModel):
  18. _name = 'account.bank.statement.import.paypal.parser'
  19. _description = 'Account Bank Statement Import PayPal Parser'
  20. @api.model
  21. def parse_header(self, data_file):
  22. data = StringIO(data_file.decode('utf-8-sig'))
  23. csv_data = reader(data)
  24. return list(next(csv_data))
  25. @api.model
  26. def parse(self, mapping, data_file, filename):
  27. journal = self.env['account.journal'].browse(
  28. self.env.context.get('journal_id')
  29. )
  30. currency_code = (
  31. journal.currency_id or journal.company_id.currency_id
  32. ).name
  33. account_number = journal.bank_account_id.acc_number
  34. name = _('%s: %s') % (
  35. journal.code,
  36. path.basename(filename),
  37. )
  38. lines = self._parse_lines(mapping, data_file, currency_code)
  39. if not lines:
  40. return currency_code, account_number, [{
  41. 'name': name,
  42. 'transactions': [],
  43. }]
  44. lines = list(sorted(
  45. lines,
  46. key=lambda line: line['timestamp']
  47. ))
  48. first_line = lines[0]
  49. balance_start = first_line['balance_amount']
  50. balance_start -= first_line['gross_amount']
  51. balance_start -= first_line['fee_amount']
  52. last_line = lines[-1]
  53. balance_end = last_line['balance_amount']
  54. transactions = list(itertools.chain.from_iterable(map(
  55. lambda line: self._convert_line_to_transactions(line),
  56. lines
  57. )))
  58. return currency_code, account_number, [{
  59. 'name': name,
  60. 'date': first_line['timestamp'].date(),
  61. 'balance_start': float(balance_start),
  62. 'balance_end_real': float(balance_end),
  63. 'transactions': transactions,
  64. }]
  65. def _parse_lines(self, mapping, data_file, currency_code):
  66. data = StringIO(data_file.decode('utf-8-sig'))
  67. csv_data = reader(data)
  68. header = list(next(csv_data))
  69. date_column = header.index(mapping.date_column)
  70. time_column = header.index(mapping.time_column)
  71. tz_column = header.index(mapping.tz_column)
  72. name_column = header.index(mapping.name_column)
  73. currency_column = header.index(mapping.currency_column)
  74. gross_column = header.index(mapping.gross_column)
  75. fee_column = header.index(mapping.fee_column)
  76. balance_column = header.index(mapping.balance_column)
  77. transaction_id_column = header.index(mapping.transaction_id_column)
  78. try:
  79. description_column = header.index(mapping.description_column)
  80. except ValueError:
  81. description_column = None
  82. try:
  83. type_column = header.index(mapping.type_column)
  84. except ValueError:
  85. type_column = None
  86. try:
  87. from_email_address_column = header.index(
  88. mapping.from_email_address_column
  89. )
  90. except ValueError:
  91. from_email_address_column = None
  92. try:
  93. to_email_address_column = header.index(
  94. mapping.to_email_address_column
  95. )
  96. except ValueError:
  97. to_email_address_column = None
  98. try:
  99. invoice_id_column = header.index(mapping.invoice_id_column)
  100. except ValueError:
  101. invoice_id_column = None
  102. try:
  103. subject_column = header.index(mapping.subject_column)
  104. except ValueError:
  105. subject_column = None
  106. try:
  107. note_column = header.index(mapping.note_column)
  108. except ValueError:
  109. note_column = None
  110. try:
  111. bank_name_column = header.index(mapping.bank_name_column)
  112. except ValueError:
  113. bank_name_column = None
  114. try:
  115. bank_account_column = header.index(mapping.bank_account_column)
  116. except ValueError:
  117. bank_account_column = None
  118. lines = []
  119. for row in csv_data:
  120. row = list(row)
  121. date_value = row[date_column]
  122. time_value = row[time_column]
  123. tz_value = row[tz_column]
  124. name_value = row[name_column]
  125. currency_value = row[currency_column]
  126. gross_value = row[gross_column]
  127. fee_value = row[fee_column]
  128. balance_value = row[balance_column]
  129. transaction_id_value = row[transaction_id_column]
  130. description_value = row[description_column] \
  131. if description_column is not None else None
  132. type_value = row[type_column] \
  133. if type_column is not None else None
  134. from_email_address_value = row[from_email_address_column] \
  135. if from_email_address_column is not None else None
  136. to_email_address_value = row[to_email_address_column] \
  137. if to_email_address_column is not None else None
  138. invoice_id_value = row[invoice_id_column] \
  139. if invoice_id_column is not None else None
  140. subject_value = row[subject_column] \
  141. if subject_column is not None else None
  142. note_value = row[note_column] \
  143. if note_column is not None else None
  144. bank_name_value = row[bank_name_column] \
  145. if bank_name_column is not None else None
  146. bank_account_value = row[bank_account_column] \
  147. if bank_account_column is not None else None
  148. if currency_value != currency_code:
  149. continue
  150. date = datetime.strptime(date_value, mapping.date_format).date()
  151. time = datetime.strptime(time_value, mapping.time_format).time()
  152. timestamp = datetime.combine(date, time)
  153. tz_value = self._normalize_tz(tz_value)
  154. tz = timezone(tz_value)
  155. timestamp = timestamp.replace(tzinfo=tz)
  156. timestamp = timestamp.astimezone(utc).replace(tzinfo=None)
  157. gross_amount = self._parse_decimal(gross_value, mapping)
  158. fee_amount = self._parse_decimal(fee_value, mapping)
  159. balance_amount = self._parse_decimal(balance_value, mapping)
  160. bank = '%s - %s' % (
  161. bank_name_value,
  162. bank_account_value,
  163. ) if bank_name_value and bank_account_value else None
  164. if to_email_address_column is None:
  165. payer_email = from_email_address_value
  166. else:
  167. payer_email = to_email_address_value \
  168. if gross_amount < 0.0 else from_email_address_value
  169. lines.append({
  170. 'transaction_id': transaction_id_value,
  171. 'invoice': invoice_id_value,
  172. 'description': description_value or type_value,
  173. 'details': subject_value or note_value or bank,
  174. 'timestamp': timestamp,
  175. 'gross_amount': gross_amount,
  176. 'fee_amount': fee_amount,
  177. 'balance_amount': balance_amount,
  178. 'payer_name': name_value,
  179. 'payer_email': payer_email,
  180. 'partner_bank_name': bank_name_value,
  181. 'partner_bank_account': bank_account_value,
  182. })
  183. return lines
  184. @api.model
  185. def _convert_line_to_transactions(self, line):
  186. transactions = []
  187. transaction_id = line['transaction_id']
  188. invoice = line['invoice']
  189. description = line['description']
  190. details = line['details']
  191. timestamp = line['timestamp']
  192. gross_amount = line['gross_amount']
  193. fee_amount = line['fee_amount']
  194. payer_name = line['payer_name']
  195. payer_email = line['payer_email']
  196. partner_bank_account = line['partner_bank_account']
  197. if invoice:
  198. invoice = _('Invoice %s') % invoice
  199. note = '%s %s' % (
  200. description,
  201. transaction_id,
  202. )
  203. if details:
  204. note += ': %s' % details
  205. if payer_email:
  206. note += ' (%s)' % payer_email
  207. unique_import_id = '%s-%s' % (
  208. transaction_id,
  209. int(timestamp.timestamp()),
  210. )
  211. name = invoice or details or description or '',
  212. transaction = {
  213. 'name': invoice or details or description or '',
  214. 'amount': str(gross_amount),
  215. 'date': timestamp,
  216. 'note': note,
  217. 'unique_import_id': unique_import_id,
  218. }
  219. if payer_name:
  220. line.update({
  221. 'partner_name': payer_name,
  222. })
  223. if partner_bank_account:
  224. line.update({
  225. 'account_number': partner_bank_account,
  226. })
  227. transactions.append(transaction)
  228. if fee_amount:
  229. transactions.append({
  230. 'name': _('Fee for %s') % (name or transaction_id),
  231. 'amount': str(fee_amount),
  232. 'date': timestamp,
  233. 'partner_name': 'PayPal',
  234. 'unique_import_id': '%s-FEE' % unique_import_id,
  235. 'note': _('Transaction fee for %s') % note,
  236. })
  237. return transactions
  238. @api.model
  239. def _parse_decimal(self, value, mapping):
  240. thousands, decimal = mapping._get_float_separators()
  241. value = value.replace(thousands, '')
  242. value = value.replace(decimal, '.')
  243. return Decimal(value)
  244. @api.model
  245. def _normalize_tz(self, value):
  246. if value in ['PDT', 'PST']:
  247. return 'America/Los_Angeles'
  248. elif value in ['CET', 'CEST']:
  249. return 'Europe/Paris'
  250. return value