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.

404 lines
14 KiB

  1. # Copyright 2019-2020 Brainbean Apps (https://brainbeanapps.com)
  2. # Copyright 2019-2020 Dataplug (https://dataplug.io)
  3. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
  4. from dateutil.relativedelta import relativedelta, MO
  5. from decimal import Decimal
  6. import logging
  7. from sys import exc_info
  8. from odoo import models, fields, api, _
  9. from odoo.addons.base.models.res_bank import sanitize_account_number
  10. _logger = logging.getLogger(__name__)
  11. class OnlineBankStatementProvider(models.Model):
  12. _name = 'online.bank.statement.provider'
  13. _inherit = ['mail.thread']
  14. _description = 'Online Bank Statement Provider'
  15. company_id = fields.Many2one(
  16. related='journal_id.company_id',
  17. store=True,
  18. )
  19. active = fields.Boolean()
  20. name = fields.Char(
  21. string='Name',
  22. compute='_compute_name',
  23. store=True,
  24. )
  25. journal_id = fields.Many2one(
  26. comodel_name='account.journal',
  27. required=True,
  28. readonly=True,
  29. ondelete='cascade',
  30. domain=[
  31. ('type', '=', 'bank'),
  32. ],
  33. )
  34. currency_id = fields.Many2one(
  35. related='journal_id.currency_id',
  36. )
  37. account_number = fields.Char(
  38. related='journal_id.bank_account_id.sanitized_acc_number'
  39. )
  40. service = fields.Selection(
  41. selection=lambda self: self._selection_service(),
  42. required=True,
  43. readonly=True,
  44. )
  45. interval_type = fields.Selection(
  46. selection=[
  47. ('minutes', 'Minute(s)'),
  48. ('hours', 'Hour(s)'),
  49. ('days', 'Day(s)'),
  50. ('weeks', 'Week(s)'),
  51. ],
  52. default='hours',
  53. required=True,
  54. )
  55. interval_number = fields.Integer(
  56. string='Scheduled update interval',
  57. default=1,
  58. required=True,
  59. )
  60. update_schedule = fields.Char(
  61. string='Update Schedule',
  62. compute='_compute_update_schedule',
  63. )
  64. last_successful_run = fields.Datetime(
  65. string='Last successful pull',
  66. )
  67. next_run = fields.Datetime(
  68. string='Next scheduled pull',
  69. default=fields.Datetime.now,
  70. required=True,
  71. )
  72. statement_creation_mode = fields.Selection(
  73. selection=[
  74. ('daily', 'Daily statements'),
  75. ('weekly', 'Weekly statements'),
  76. ('monthly', 'Monthly statements'),
  77. ],
  78. default='daily',
  79. required=True,
  80. )
  81. api_base = fields.Char()
  82. origin = fields.Char()
  83. username = fields.Char()
  84. password = fields.Char()
  85. key = fields.Binary()
  86. certificate = fields.Binary()
  87. passphrase = fields.Char()
  88. certificate_public_key = fields.Text()
  89. certificate_private_key = fields.Text()
  90. certificate_chain = fields.Text()
  91. _sql_constraints = [
  92. (
  93. 'journal_id_uniq',
  94. 'UNIQUE(journal_id)',
  95. 'Only one online banking statement provider per journal!'
  96. ),
  97. (
  98. 'valid_interval_number',
  99. 'CHECK(interval_number > 0)',
  100. 'Scheduled update interval must be greater than zero!'
  101. )
  102. ]
  103. @api.model
  104. def _get_available_services(self):
  105. """Hook for extension"""
  106. return []
  107. @api.model
  108. def _selection_service(self):
  109. return self._get_available_services() + [('dummy', 'Dummy')]
  110. @api.model
  111. def values_service(self):
  112. return self._get_available_services()
  113. @api.multi
  114. @api.depends('service')
  115. def _compute_name(self):
  116. for provider in self:
  117. provider.name = list(filter(
  118. lambda x: x[0] == provider.service,
  119. self._selection_service()
  120. ))[0][1]
  121. @api.multi
  122. @api.depends('active', 'interval_type', 'interval_number')
  123. def _compute_update_schedule(self):
  124. for provider in self:
  125. if not provider.active:
  126. provider.update_schedule = _('Inactive')
  127. continue
  128. provider.update_schedule = _('%(number)s %(type)s') % {
  129. 'number': provider.interval_number,
  130. 'type': list(filter(
  131. lambda x: x[0] == provider.interval_type,
  132. self._fields['interval_type'].selection
  133. ))[0][1],
  134. }
  135. @api.multi
  136. def _pull(self, date_since, date_until):
  137. AccountBankStatement = self.env['account.bank.statement']
  138. is_scheduled = self.env.context.get('scheduled')
  139. if is_scheduled:
  140. AccountBankStatement = AccountBankStatement.with_context(
  141. tracking_disable=True,
  142. )
  143. AccountBankStatementLine = self.env['account.bank.statement.line']
  144. for provider in self:
  145. statement_date_since = provider._get_statement_date_since(
  146. date_since
  147. )
  148. while statement_date_since < date_until:
  149. statement_date_until = (
  150. statement_date_since + provider._get_statement_date_step()
  151. )
  152. try:
  153. data = provider._obtain_statement_data(
  154. statement_date_since,
  155. statement_date_until
  156. )
  157. except:
  158. e = exc_info()[1]
  159. if is_scheduled:
  160. _logger.warning(
  161. 'Online Bank Statement Provider "%s" failed to'
  162. ' obtain statement data since %s until %s' % (
  163. provider.name,
  164. statement_date_since,
  165. statement_date_until,
  166. ),
  167. exc_info=True,
  168. )
  169. provider.message_post(
  170. body=_(
  171. 'Online Bank Statement Provider "%s" failed to'
  172. ' obtain statement data since %s until %s:\n%s'
  173. ) % (
  174. provider.name,
  175. statement_date_since,
  176. statement_date_until,
  177. str(e) if e else _('N/A'),
  178. ),
  179. subject=_(
  180. 'Online Bank Statement Provider failure'
  181. ),
  182. )
  183. break
  184. raise
  185. statement_date = provider._get_statement_date(
  186. statement_date_since,
  187. statement_date_until,
  188. )
  189. if not data:
  190. statement_date_since = statement_date_until
  191. continue
  192. lines_data, statement_values = data
  193. statement = AccountBankStatement.search([
  194. ('journal_id', '=', provider.journal_id.id),
  195. ('state', '=', 'open'),
  196. ('date', '=', statement_date),
  197. ], limit=1)
  198. if not statement:
  199. statement_values.update({
  200. 'name': provider.journal_id.sequence_id.with_context(
  201. ir_sequence_date=statement_date,
  202. ).next_by_id(),
  203. 'journal_id': provider.journal_id.id,
  204. 'date': statement_date,
  205. })
  206. statement = AccountBankStatement.with_context(
  207. journal_id=provider.journal_id.id,
  208. ).create(
  209. # NOTE: This is needed since create() alters values
  210. statement_values.copy()
  211. )
  212. filtered_lines = []
  213. for line_values in lines_data:
  214. date = fields.Datetime.from_string(line_values['date'])
  215. if date < statement_date_since or date < date_since:
  216. if 'balance_start' in statement_values:
  217. statement_values['balance_start'] = (
  218. Decimal(
  219. statement_values['balance_start']
  220. ) + Decimal(
  221. line_values['amount']
  222. )
  223. )
  224. continue
  225. elif date >= statement_date_until or date >= date_until:
  226. if 'balance_end_real' in statement_values:
  227. statement_values['balance_end_real'] = (
  228. Decimal(
  229. statement_values['balance_end_real']
  230. ) - Decimal(
  231. line_values['amount']
  232. )
  233. )
  234. continue
  235. unique_import_id = line_values.get('unique_import_id')
  236. if unique_import_id:
  237. unique_import_id = provider._generate_unique_import_id(
  238. unique_import_id
  239. )
  240. line_values.update({
  241. 'unique_import_id': unique_import_id,
  242. })
  243. if AccountBankStatementLine.sudo().search(
  244. [('unique_import_id', '=', unique_import_id)],
  245. limit=1):
  246. continue
  247. bank_account_number = line_values.get('account_number')
  248. if bank_account_number:
  249. line_values.update({
  250. 'account_number': (
  251. self._sanitize_bank_account_number(
  252. bank_account_number
  253. )
  254. ),
  255. })
  256. filtered_lines.append(line_values)
  257. statement_values.update({
  258. 'line_ids': [[0, False, line] for line in filtered_lines],
  259. })
  260. if 'balance_start' in statement_values:
  261. statement_values['balance_start'] = float(
  262. statement_values['balance_start']
  263. )
  264. if 'balance_end_real' in statement_values:
  265. statement_values['balance_end_real'] = float(
  266. statement_values['balance_end_real']
  267. )
  268. statement.write(statement_values)
  269. statement_date_since = statement_date_until
  270. if is_scheduled:
  271. provider._schedule_next_run()
  272. @api.multi
  273. def _schedule_next_run(self):
  274. self.ensure_one()
  275. self.last_successful_run = self.next_run
  276. self.next_run += self._get_next_run_period()
  277. @api.multi
  278. def _get_statement_date_since(self, date):
  279. self.ensure_one()
  280. date = date.replace(
  281. hour=0,
  282. minute=0,
  283. second=0,
  284. microsecond=0,
  285. )
  286. if self.statement_creation_mode == 'daily':
  287. return date
  288. elif self.statement_creation_mode == 'weekly':
  289. return date + relativedelta(weekday=MO(-1))
  290. elif self.statement_creation_mode == 'monthly':
  291. return date.replace(
  292. day=1,
  293. )
  294. @api.multi
  295. def _get_statement_date_step(self):
  296. self.ensure_one()
  297. if self.statement_creation_mode == 'daily':
  298. return relativedelta(
  299. days=1,
  300. hour=0,
  301. minute=0,
  302. second=0,
  303. microsecond=0,
  304. )
  305. elif self.statement_creation_mode == 'weekly':
  306. return relativedelta(
  307. weeks=1,
  308. weekday=MO,
  309. hour=0,
  310. minute=0,
  311. second=0,
  312. microsecond=0,
  313. )
  314. elif self.statement_creation_mode == 'monthly':
  315. return relativedelta(
  316. months=1,
  317. day=1,
  318. hour=0,
  319. minute=0,
  320. second=0,
  321. microsecond=0,
  322. )
  323. @api.multi
  324. def _get_statement_date(self, date_since, date_until):
  325. self.ensure_one()
  326. # NOTE: Statement date is treated by Odoo as start of period. Details
  327. # - addons/account/models/account_journal_dashboard.py
  328. # - def get_line_graph_datas()
  329. return date_since.date()
  330. @api.multi
  331. def _generate_unique_import_id(self, unique_import_id):
  332. self.ensure_one()
  333. return (
  334. self.account_number and self.account_number + '-' or ''
  335. ) + str(self.journal_id.id) + '-' + unique_import_id
  336. @api.multi
  337. def _sanitize_bank_account_number(self, bank_account_number):
  338. """Hook for extension"""
  339. self.ensure_one()
  340. return sanitize_account_number(bank_account_number)
  341. @api.multi
  342. def _get_next_run_period(self):
  343. self.ensure_one()
  344. if self.interval_type == 'minutes':
  345. return relativedelta(minutes=self.interval_number)
  346. elif self.interval_type == 'hours':
  347. return relativedelta(hours=self.interval_number)
  348. elif self.interval_type == 'days':
  349. return relativedelta(days=self.interval_number)
  350. elif self.interval_type == 'weeks':
  351. return relativedelta(weeks=self.interval_number)
  352. @api.model
  353. def _scheduled_pull(self):
  354. _logger.info('Scheduled pull of online bank statements...')
  355. providers = self.search([
  356. ('active', '=', True),
  357. ('next_run', '<=', fields.Datetime.now()),
  358. ])
  359. if providers:
  360. _logger.info('Pulling online bank statements of: %s' % ', '.join(
  361. providers.mapped('journal_id.name')
  362. ))
  363. for provider in providers.with_context({'scheduled': True}):
  364. date_since = (
  365. provider.last_successful_run
  366. ) if provider.last_successful_run else (
  367. provider.next_run - provider._get_next_run_period()
  368. )
  369. date_until = provider.next_run
  370. provider._pull(date_since, date_until)
  371. _logger.info('Scheduled pull of online bank statements complete.')
  372. @api.multi
  373. def _obtain_statement_data(
  374. self, date_since, date_until
  375. ):
  376. """Hook for extension"""
  377. # Check tests/online_bank_statement_provider_dummy.py for reference
  378. self.ensure_one()
  379. return []