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.

297 lines
11 KiB

  1. # Copyright 2014-2017 Akretion (http://www.akretion.com).
  2. # @author Alexis de Lattre <alexis.delattre@akretion.com>
  3. # @author Sébastien BEAU <sebastien.beau@akretion.com>
  4. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
  5. import logging
  6. from datetime import datetime
  7. from odoo import _, api, fields, models
  8. from odoo.exceptions import UserError
  9. import re
  10. from io import StringIO
  11. _logger = logging.getLogger(__name__)
  12. try:
  13. import csv
  14. except (ImportError, IOError) as err:
  15. _logger.debug(err)
  16. # Paypal header depend of the country the order are the same but the
  17. # value are translated. You can add you header here
  18. HEADERS = [
  19. # French
  20. '"Date","Heure","Fuseau horaire","Description","Devise","Avant commission"'
  21. ',"Commission","Net","Solde","Numéro de transaction","Adresse email de '
  22. 'l\'expéditeur","Nom","Nom de la banque","Compte bancaire","Montant des '
  23. 'frais de livraison et de traitement","TVA","Identifiant de facture",'
  24. '"Numéro de la transaction de référence"',
  25. # English
  26. '"Date","Time","Time Zone","Description","Currency","Gross ","Fee ","Net",'
  27. '"Balance","Transaction ID","From Email Address","Name","Bank Name",'
  28. '"Bank Account","Shipping and Handling Amount","Sales Tax","Invoice ID",'
  29. '"Reference Txn ID"',
  30. ]
  31. class AccountBankStatementImport(models.TransientModel):
  32. _inherit = 'account.bank.statement.import'
  33. paypal_map_id = fields.Many2one(
  34. comodel_name='account.bank.statement.import.paypal.map',
  35. string='Paypal map',
  36. readonly=True,
  37. )
  38. @api.model
  39. def _get_paypal_encoding(self):
  40. return 'utf-8-sig'
  41. @api.model
  42. def _get_paypal_str_data(self, data_file):
  43. if not isinstance(data_file, str):
  44. data_file = data_file.decode(self._get_paypal_encoding())
  45. return data_file.strip()
  46. @api.model
  47. def _paypal_convert_amount(self, amount_str):
  48. if self.paypal_map_id:
  49. thousands, decimal = self.paypal_map_id._get_separators()
  50. else:
  51. thousands, decimal = ',', '.'
  52. valstr = re.sub(r'[^\d%s%s.-]' % (thousands, decimal), '', amount_str)
  53. valstrdot = valstr.replace(thousands, '')
  54. valstrdot = valstrdot.replace(decimal, '.')
  55. return float(valstrdot)
  56. @api.model
  57. def _check_paypal(self, data_file):
  58. data_file = self._get_paypal_str_data(data_file)
  59. if not self.paypal_map_id:
  60. return False
  61. headers = self.mapped('paypal_map_id.map_line_ids.name')
  62. file_headers = data_file.split('\n', 1)[0]
  63. if any(item not in file_headers for item in headers):
  64. raise UserError(
  65. _("Headers of file to import and Paypal map lines does not "
  66. "match."))
  67. return True
  68. def _convert_paypal_line_to_dict(self, idx, line):
  69. rline = dict()
  70. for item in range(len(line)):
  71. paypal_map = self.mapped('paypal_map_id.map_line_ids')[item]
  72. value = line[item]
  73. if not paypal_map.field_to_assign:
  74. continue
  75. if paypal_map.date_format:
  76. try:
  77. value = fields.Date.to_string(
  78. datetime.strptime(value, paypal_map.date_format))
  79. except Exception:
  80. raise UserError(
  81. _("Date format of map file and Paypal date does "
  82. "not match."))
  83. rline[paypal_map.field_to_assign] = value
  84. for field in ['commission', 'amount', 'balance']:
  85. _logger.debug('Trying to convert %s to float' % rline[field])
  86. try:
  87. rline[field] = self._paypal_convert_amount(rline[field])
  88. except Exception:
  89. raise UserError(
  90. _("Value '%s' for the field '%s' on line %d, "
  91. "cannot be converted to float")
  92. % (rline[field], field, idx))
  93. return rline
  94. def _parse_paypal_file(self, data_file):
  95. data_file = self._get_paypal_str_data(data_file)
  96. f = StringIO(data_file)
  97. f.seek(0)
  98. raw_lines = []
  99. reader = csv.reader(f)
  100. next(reader) # Drop header
  101. for idx, line in enumerate(reader):
  102. _logger.debug("Line %d: %s" % (idx, line))
  103. raw_lines.append(self._convert_paypal_line_to_dict(idx, line))
  104. return raw_lines
  105. def _prepare_paypal_currency_vals(self, cline):
  106. currencies = self.env['res.currency'].search(
  107. [('name', '=', cline['currency'])])
  108. if not currencies:
  109. raise UserError(
  110. _('currency %s on line %d cannot be found in odoo')
  111. % (cline['currency'], cline['idx']))
  112. return {
  113. 'amount_currency': cline['amount'],
  114. 'currency_id': currencies.id,
  115. 'currency': cline['currency'],
  116. 'partner_name': cline['partner_name'],
  117. 'description': cline['description'],
  118. 'email': cline['email'],
  119. 'transaction_id': cline['transaction_id'],
  120. }
  121. def _get_journal(self):
  122. journal_id = self.env.context.get('journal_id')
  123. if not journal_id:
  124. raise UserError(_('You must run this wizard from the journal'))
  125. return self.env['account.journal'].browse(journal_id)
  126. def _post_process_statement_line(self, raw_lines):
  127. journal = self._get_journal()
  128. currency = journal.currency_id or journal.company_id.currency_id
  129. currency_change_lines = {}
  130. real_transactions = []
  131. for line in raw_lines:
  132. if line['currency'] != currency.name:
  133. currency_change_lines[line['transaction_id']] = line
  134. else:
  135. real_transactions.append(line)
  136. for line in real_transactions:
  137. # Check if the current transaction is linked with a
  138. # transaction of currency change if yes merge the transaction
  139. # as for odoo it's only one line
  140. cline = currency_change_lines.get(line['origin_transaction_id'])
  141. if cline:
  142. # we update the current line with currency information
  143. vals = self._prepare_paypal_currency_vals(cline)
  144. line.update(vals)
  145. return real_transactions
  146. def _prepare_paypal_statement_line(self, fline):
  147. if fline['bank_name']:
  148. name = '|'.join([
  149. fline['description'],
  150. fline['bank_name'],
  151. fline['bank_account']
  152. ])
  153. else:
  154. name = '|'.join([
  155. fline['description'],
  156. fline['partner_name'],
  157. fline['email'],
  158. fline['invoice_number'],
  159. ])
  160. return {
  161. 'date': fline['date'],
  162. 'name': name,
  163. 'ref': fline['transaction_id'],
  164. 'unique_import_id':
  165. fline['transaction_id'] + fline['date'] + fline['time'],
  166. 'amount': fline['amount'],
  167. 'bank_account_id': False,
  168. 'currency_id': fline.get('currency_id'),
  169. 'amount_currency': fline.get('amount_currency'),
  170. }
  171. def _prepare_paypal_statement(self, lines):
  172. return {
  173. 'name':
  174. _('PayPal Import %s > %s')
  175. % (lines[0]['date'], lines[-1]['date']),
  176. 'date': lines[-1]['date'],
  177. 'balance_start':
  178. lines[0]['balance'] -
  179. lines[0]['amount'] -
  180. lines[0]['commission'],
  181. 'balance_end_real': lines[-1]['balance'],
  182. }
  183. @api.model
  184. def _parse_file(self, data_file):
  185. """ Import a file in Paypal CSV format """
  186. paypal = self._check_paypal(data_file)
  187. if not paypal:
  188. return super(AccountBankStatementImport, self)._parse_file(
  189. data_file)
  190. raw_lines = self._parse_paypal_file(data_file)
  191. final_lines = self._post_process_statement_line(raw_lines)
  192. vals_bank_statement = self._prepare_paypal_statement(final_lines)
  193. transactions = []
  194. commission_total = 0
  195. for fline in final_lines:
  196. commission_total += fline['commission']
  197. vals_line = self._prepare_paypal_statement_line(fline)
  198. _logger.debug("vals_line = %s" % vals_line)
  199. transactions.append(vals_line)
  200. if commission_total:
  201. commission_line = {
  202. 'date': vals_bank_statement['date'],
  203. 'name': _('Paypal commissions'),
  204. 'ref': _('PAYPAL-COSTS'),
  205. 'amount': commission_total,
  206. 'unique_import_id': False,
  207. }
  208. transactions.append(commission_line)
  209. vals_bank_statement['transactions'] = transactions
  210. return None, None, [vals_bank_statement]
  211. @api.model
  212. def _get_paypal_partner(self, description, partner_name,
  213. partner_email, invoice_number):
  214. if invoice_number:
  215. # In most case e-commerce case invoice_number
  216. # will contain the sale order number
  217. sale = self.env['sale.order'].search([
  218. ('name', '=', invoice_number)])
  219. if sale and len(sale) == 1:
  220. return sale.partner_id.commercial_partner_id
  221. invoice = self.env['account.invoice'].search([
  222. ('number', '=', invoice_number)])
  223. if invoice and len(invoice) == 1:
  224. return invoice.partner_id.commercial_partner_id
  225. if partner_email:
  226. partner = self.env['res.partner'].search([
  227. ('email', '=', partner_email),
  228. ('parent_id', '=', False)])
  229. if partner and len(partner) == 1:
  230. return partner.commercial_partner_id
  231. if partner_name:
  232. partner = self.env['res.partner'].search([
  233. ('name', '=ilike', partner_name)])
  234. if partner and len(partner) == 1:
  235. return partner.commercial_partner_id
  236. return None
  237. @api.model
  238. def _complete_paypal_statement_line(self, line):
  239. _logger.debug('Process line %s', line['name'])
  240. info = line['name'].split('|')
  241. if len(info) == 4:
  242. partner = self._get_paypal_partner(*info)
  243. if partner:
  244. return {
  245. 'partner_id': partner.id,
  246. 'account_id': partner.property_account_receivable_id.id,
  247. }
  248. return None
  249. @api.model
  250. def _complete_stmts_vals(self, stmts_vals, journal_id, account_number):
  251. """ Match the partner from paypal information """
  252. stmts_vals = super(AccountBankStatementImport, self). \
  253. _complete_stmts_vals(stmts_vals, journal_id, account_number)
  254. for line in stmts_vals[0]['transactions']:
  255. vals = self._complete_paypal_statement_line(line)
  256. if vals:
  257. line.update(vals)
  258. return stmts_vals
  259. @api.model
  260. def default_get(self, fields):
  261. res = super(AccountBankStatementImport, self).default_get(fields)
  262. journal = self._get_journal()
  263. res['paypal_map_id'] = journal.paypal_map_id.id
  264. return res