payment_transaction.py 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038
  1. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  2. import logging
  3. import pprint
  4. import re
  5. import unicodedata
  6. from datetime import datetime
  7. import psycopg2
  8. from dateutil import relativedelta
  9. from odoo import _, api, fields, models
  10. from odoo.exceptions import UserError, ValidationError
  11. from odoo.tools import consteq, format_amount, ustr
  12. from odoo.tools.misc import hmac as hmac_tool
  13. from odoo.addons.payment import utils as payment_utils
  14. _logger = logging.getLogger(__name__)
  15. class PaymentTransaction(models.Model):
  16. _name = 'payment.transaction'
  17. _description = 'Payment Transaction'
  18. _order = 'id desc'
  19. _rec_name = 'reference'
  20. @api.model
  21. def _lang_get(self):
  22. return self.env['res.lang'].get_installed()
  23. provider_id = fields.Many2one(
  24. string="Provider", comodel_name='payment.provider', readonly=True, required=True)
  25. provider_code = fields.Selection(related='provider_id.code')
  26. company_id = fields.Many2one( # Indexed to speed-up ORM searches (from ir_rule or others)
  27. related='provider_id.company_id', store=True, index=True)
  28. reference = fields.Char(
  29. string="Reference", help="The internal reference of the transaction", readonly=True,
  30. required=True) # Already has an index from the UNIQUE SQL constraint.
  31. provider_reference = fields.Char(
  32. string="Provider Reference", help="The provider reference of the transaction",
  33. readonly=True) # This is not the same thing as the provider reference of the token.
  34. amount = fields.Monetary(
  35. string="Amount", currency_field='currency_id', readonly=True, required=True)
  36. currency_id = fields.Many2one(
  37. string="Currency", comodel_name='res.currency', readonly=True, required=True)
  38. fees = fields.Monetary(
  39. string="Fees", currency_field='currency_id',
  40. help="The fees amount; set by the system as it depends on the provider", readonly=True)
  41. token_id = fields.Many2one(
  42. string="Payment Token", comodel_name='payment.token', readonly=True,
  43. domain='[("provider_id", "=", "provider_id")]', ondelete='restrict')
  44. state = fields.Selection(
  45. string="Status",
  46. selection=[('draft', "Draft"), ('pending', "Pending"), ('authorized', "Authorized"),
  47. ('done', "Confirmed"), ('cancel', "Canceled"), ('error', "Error")],
  48. default='draft', readonly=True, required=True, copy=False, index=True)
  49. state_message = fields.Text(
  50. string="Message", help="The complementary information message about the state",
  51. readonly=True)
  52. last_state_change = fields.Datetime(
  53. string="Last State Change Date", readonly=True, default=fields.Datetime.now)
  54. # Fields used for traceability.
  55. operation = fields.Selection( # This should not be trusted if the state is draft or pending.
  56. string="Operation",
  57. selection=[
  58. ('online_redirect', "Online payment with redirection"),
  59. ('online_direct', "Online direct payment"),
  60. ('online_token', "Online payment by token"),
  61. ('validation', "Validation of the payment method"),
  62. ('offline', "Offline payment by token"),
  63. ('refund', "Refund")
  64. ],
  65. readonly=True,
  66. index=True,
  67. )
  68. source_transaction_id = fields.Many2one(
  69. string="Source Transaction",
  70. comodel_name='payment.transaction',
  71. help="The source transaction of related refund transactions",
  72. readonly=True,
  73. )
  74. child_transaction_ids = fields.One2many(
  75. string="Child Transactions",
  76. help="The child transactions of the source transaction.",
  77. comodel_name='payment.transaction',
  78. inverse_name='source_transaction_id',
  79. readonly=True,
  80. )
  81. refunds_count = fields.Integer(string="Refunds Count", compute='_compute_refunds_count')
  82. # Fields used for user redirection & payment post-processing
  83. is_post_processed = fields.Boolean(
  84. string="Is Post-processed", help="Has the payment been post-processed")
  85. tokenize = fields.Boolean(
  86. string="Create Token",
  87. help="Whether a payment token should be created when post-processing the transaction")
  88. landing_route = fields.Char(
  89. string="Landing Route",
  90. help="The route the user is redirected to after the transaction")
  91. callback_model_id = fields.Many2one(
  92. string="Callback Document Model", comodel_name='ir.model', groups='base.group_system')
  93. callback_res_id = fields.Integer(string="Callback Record ID", groups='base.group_system')
  94. callback_method = fields.Char(string="Callback Method", groups='base.group_system')
  95. # Hash for extra security on top of the callback fields' group in case a bug exposes a sudo.
  96. callback_hash = fields.Char(string="Callback Hash", groups='base.group_system')
  97. callback_is_done = fields.Boolean(
  98. string="Callback Done", help="Whether the callback has already been executed",
  99. groups="base.group_system", readonly=True)
  100. # Duplicated partner values allowing to keep a record of them, should they be later updated.
  101. partner_id = fields.Many2one(
  102. string="Customer", comodel_name='res.partner', readonly=True, required=True,
  103. ondelete='restrict')
  104. partner_name = fields.Char(string="Partner Name")
  105. partner_lang = fields.Selection(string="Language", selection=_lang_get)
  106. partner_email = fields.Char(string="Email")
  107. partner_address = fields.Char(string="Address")
  108. partner_zip = fields.Char(string="Zip")
  109. partner_city = fields.Char(string="City")
  110. partner_state_id = fields.Many2one(string="State", comodel_name='res.country.state')
  111. partner_country_id = fields.Many2one(string="Country", comodel_name='res.country')
  112. partner_phone = fields.Char(string="Phone")
  113. _sql_constraints = [
  114. ('reference_uniq', 'unique(reference)', "Reference must be unique!"),
  115. ]
  116. #=== COMPUTE METHODS ===#
  117. def _compute_refunds_count(self):
  118. rg_data = self.env['payment.transaction']._read_group(
  119. domain=[('source_transaction_id', 'in', self.ids), ('operation', '=', 'refund')],
  120. fields=['source_transaction_id'],
  121. groupby=['source_transaction_id'],
  122. )
  123. data = {x['source_transaction_id'][0]: x['source_transaction_id_count'] for x in rg_data}
  124. for record in self:
  125. record.refunds_count = data.get(record.id, 0)
  126. #=== CONSTRAINT METHODS ===#
  127. @api.constrains('state')
  128. def _check_state_authorized_supported(self):
  129. """ Check that authorization is supported for a transaction in the `authorized` state. """
  130. illegal_authorize_state_txs = self.filtered(
  131. lambda tx: tx.state == 'authorized' and not tx.provider_id.support_manual_capture
  132. )
  133. if illegal_authorize_state_txs:
  134. raise ValidationError(_(
  135. "Transaction authorization is not supported by the following payment providers: %s",
  136. ', '.join(set(illegal_authorize_state_txs.mapped('provider_id.name')))
  137. ))
  138. @api.constrains('token_id')
  139. def _check_token_is_active(self):
  140. """ Check that the token used to create the transaction is active. """
  141. if self.token_id and not self.token_id.active:
  142. raise ValidationError(_("Creating a transaction from an archived token is forbidden."))
  143. #=== CRUD METHODS ===#
  144. @api.model_create_multi
  145. def create(self, values_list):
  146. for values in values_list:
  147. provider = self.env['payment.provider'].browse(values['provider_id'])
  148. if not values.get('reference'):
  149. values['reference'] = self._compute_reference(provider.code, **values)
  150. # Duplicate partner values.
  151. partner = self.env['res.partner'].browse(values['partner_id'])
  152. values.update({
  153. # Use the parent partner as fallback if the invoicing address has no name.
  154. 'partner_name': partner.name or partner.parent_id.name,
  155. 'partner_lang': partner.lang,
  156. 'partner_email': partner.email,
  157. 'partner_address': payment_utils.format_partner_address(
  158. partner.street, partner.street2
  159. ),
  160. 'partner_zip': partner.zip,
  161. 'partner_city': partner.city,
  162. 'partner_state_id': partner.state_id.id,
  163. 'partner_country_id': partner.country_id.id,
  164. 'partner_phone': partner.phone,
  165. })
  166. # Compute fees. For validation transactions, fees are zero.
  167. if values.get('operation') == 'validation':
  168. values['fees'] = 0
  169. else:
  170. currency = self.env['res.currency'].browse(values.get('currency_id')).exists()
  171. values['fees'] = provider._compute_fees(
  172. values.get('amount', 0), currency, partner.country_id,
  173. )
  174. # Include provider-specific create values
  175. values.update(self._get_specific_create_values(provider.code, values))
  176. # Generate the hash for the callback if one has be configured on the tx.
  177. values['callback_hash'] = self._generate_callback_hash(
  178. values.get('callback_model_id'),
  179. values.get('callback_res_id'),
  180. values.get('callback_method'),
  181. )
  182. txs = super().create(values_list)
  183. # Monetary fields are rounded with the currency at creation time by the ORM. Sometimes, this
  184. # can lead to inconsistent string representation of the amounts sent to the providers.
  185. # E.g., tx.create(amount=1111.11) -> tx.amount == 1111.1100000000001
  186. # To ensure a proper string representation, we invalidate this request's cache values of the
  187. # `amount` and `fees` fields for the created transactions. This forces the ORM to read the
  188. # values from the DB where there were stored using `float_repr`, which produces a result
  189. # consistent with the format expected by providers.
  190. # E.g., tx.create(amount=1111.11) ; tx.invalidate_recordset() -> tx.amount == 1111.11
  191. txs.invalidate_recordset(['amount', 'fees'])
  192. return txs
  193. @api.model
  194. def _get_specific_create_values(self, provider_code, values):
  195. """ Complete the values of the `create` method with provider-specific values.
  196. For a provider to add its own create values, it must overwrite this method and return a dict
  197. of values. Provider-specific values take precedence over those of the dict of generic create
  198. values.
  199. :param str provider_code: The code of the provider that handled the transaction.
  200. :param dict values: The original create values.
  201. :return: The dict of provider-specific create values.
  202. :rtype: dict
  203. """
  204. return dict()
  205. #=== ACTION METHODS ===#
  206. def action_view_refunds(self):
  207. """ Return the windows action to browse the refund transactions linked to the transaction.
  208. Note: `self.ensure_one()`
  209. :return: The window action to browse the refund transactions.
  210. :rtype: dict
  211. """
  212. self.ensure_one()
  213. action = {
  214. 'name': _("Refund"),
  215. 'res_model': 'payment.transaction',
  216. 'type': 'ir.actions.act_window',
  217. }
  218. if self.refunds_count == 1:
  219. refund_tx = self.env['payment.transaction'].search([
  220. ('source_transaction_id', '=', self.id),
  221. ])[0]
  222. action['res_id'] = refund_tx.id
  223. action['view_mode'] = 'form'
  224. else:
  225. action['view_mode'] = 'tree,form'
  226. action['domain'] = [('source_transaction_id', '=', self.id)]
  227. return action
  228. def action_capture(self):
  229. """ Check the state of the transactions and request their capture. """
  230. if any(tx.state != 'authorized' for tx in self):
  231. raise ValidationError(_("Only authorized transactions can be captured."))
  232. payment_utils.check_rights_on_recordset(self)
  233. for tx in self:
  234. # In sudo mode because we need to be able to read on provider fields.
  235. tx.sudo()._send_capture_request()
  236. def action_void(self):
  237. """ Check the state of the transaction and request to have them voided. """
  238. if any(tx.state != 'authorized' for tx in self):
  239. raise ValidationError(_("Only authorized transactions can be voided."))
  240. payment_utils.check_rights_on_recordset(self)
  241. for tx in self:
  242. # In sudo mode because we need to be able to read on provider fields.
  243. tx.sudo()._send_void_request()
  244. def action_refund(self, amount_to_refund=None):
  245. """ Check the state of the transactions and request their refund.
  246. :param float amount_to_refund: The amount to be refunded.
  247. :return: None
  248. """
  249. if any(tx.state != 'done' for tx in self):
  250. raise ValidationError(_("Only confirmed transactions can be refunded."))
  251. for tx in self:
  252. tx._send_refund_request(amount_to_refund)
  253. #=== BUSINESS METHODS - PAYMENT FLOW ===#
  254. @api.model
  255. def _compute_reference(self, provider_code, prefix=None, separator='-', **kwargs):
  256. """ Compute a unique reference for the transaction.
  257. The reference corresponds to the prefix if no other transaction with that prefix already
  258. exists. Otherwise, it follows the pattern `{computed_prefix}{separator}{sequence_number}`
  259. where:
  260. - `{computed_prefix}` is:
  261. - The provided custom prefix, if any.
  262. - The computation result of :meth:`_compute_reference_prefix` if the custom prefix is not
  263. filled, but the kwargs are.
  264. - `'tx-{datetime}'` if neither the custom prefix nor the kwargs are filled.
  265. - `{separator}` is the string that separates the prefix from the sequence number.
  266. - `{sequence_number}` is the next integer in the sequence of references sharing the same
  267. prefix. The sequence starts with `1` if there is only one matching reference.
  268. .. example::
  269. - Given the custom prefix `'example'` which has no match with an existing reference, the
  270. full reference will be `'example'`.
  271. - Given the custom prefix `'example'` which matches the existing reference `'example'`,
  272. and the custom separator `'-'`, the full reference will be `'example-1'`.
  273. - Given the kwargs `{'invoice_ids': [1, 2]}`, the custom separator `'-'` and no custom
  274. prefix, the full reference will be `'INV1-INV2'` (or similar) if no existing reference
  275. has the same prefix, or `'INV1-INV2-n'` if `n` existing references have the same
  276. prefix.
  277. :param str provider_code: The code of the provider handling the transaction.
  278. :param str prefix: The custom prefix used to compute the full reference.
  279. :param str separator: The custom separator used to separate the prefix from the suffix.
  280. :param dict kwargs: Optional values passed to :meth:`_compute_reference_prefix` if no custom
  281. prefix is provided.
  282. :return: The unique reference for the transaction.
  283. :rtype: str
  284. """
  285. # Compute the prefix.
  286. if prefix:
  287. # Replace special characters by their ASCII alternative (é -> e ; ä -> a ; ...)
  288. prefix = unicodedata.normalize('NFKD', prefix).encode('ascii', 'ignore').decode('utf-8')
  289. if not prefix: # Prefix not provided or voided above, compute it based on the kwargs.
  290. prefix = self.sudo()._compute_reference_prefix(provider_code, separator, **kwargs)
  291. if not prefix: # Prefix not computed from the kwargs, fallback on time-based value
  292. prefix = payment_utils.singularize_reference_prefix()
  293. # Compute the sequence number.
  294. reference = prefix # The first reference of a sequence has no sequence number.
  295. if self.sudo().search([('reference', '=', prefix)]): # The reference already has a match
  296. # We now execute a second search on `payment.transaction` to fetch all the references
  297. # starting with the given prefix. The load of these two searches is mitigated by the
  298. # index on `reference`. Although not ideal, this solution allows for quickly knowing
  299. # whether the sequence for a given prefix is already started or not, usually not. An SQL
  300. # query wouldn't help either as the selector is arbitrary and doing that would be an
  301. # open-door to SQL injections.
  302. same_prefix_references = self.sudo().search(
  303. [('reference', 'like', f'{prefix}{separator}%')]
  304. ).with_context(prefetch_fields=False).mapped('reference')
  305. # A final regex search is necessary to figure out the next sequence number. The previous
  306. # search could not rely on alphabetically sorting the reference to infer the largest
  307. # sequence number because both the prefix and the separator are arbitrary. A given
  308. # prefix could happen to be a substring of the reference from a different sequence.
  309. # For instance, the prefix 'example' is a valid match for the existing references
  310. # 'example', 'example-1' and 'example-ref', in that order. Trusting the order to infer
  311. # the sequence number would lead to a collision with 'example-1'.
  312. search_pattern = re.compile(rf'^{re.escape(prefix)}{separator}(\d+)$')
  313. max_sequence_number = 0 # If no match is found, start the sequence with this reference.
  314. for existing_reference in same_prefix_references:
  315. search_result = re.search(search_pattern, existing_reference)
  316. if search_result: # The reference has the same prefix and is from the same sequence
  317. # Find the largest sequence number, if any.
  318. current_sequence = int(search_result.group(1))
  319. if current_sequence > max_sequence_number:
  320. max_sequence_number = current_sequence
  321. # Compute the full reference.
  322. reference = f'{prefix}{separator}{max_sequence_number + 1}'
  323. return reference
  324. @api.model
  325. def _compute_reference_prefix(self, provider_code, separator, **values):
  326. """ Compute the reference prefix from the transaction values.
  327. Note: This method should be called in sudo mode to give access to the documents (invoices,
  328. sales orders) referenced in the transaction values.
  329. :param str provider_code: The code of the provider handling the transaction.
  330. :param str separator: The custom separator used to separate parts of the computed
  331. reference prefix.
  332. :param dict values: The transaction values used to compute the reference prefix.
  333. :return: The computed reference prefix.
  334. :rtype: str
  335. """
  336. return ''
  337. @api.model
  338. def _generate_callback_hash(self, callback_model_id, callback_res_id, callback_method):
  339. """ Return the hash for the callback on the transaction.
  340. :param int callback_model_id: The model on which the callback method is defined, as a
  341. `res.model` id.
  342. :param int callback_res_id: The record on which the callback method must be called, as an id
  343. of the callback method's model.
  344. :param str callback_method: The name of the callback method.
  345. :return: The callback hash.
  346. :rtype: str
  347. """
  348. if callback_model_id and callback_res_id and callback_method:
  349. model_name = self.env['ir.model'].sudo().browse(callback_model_id).model
  350. token = f'{model_name}|{callback_res_id}|{callback_method}'
  351. callback_hash = hmac_tool(self.env(su=True), 'generate_callback_hash', token)
  352. return callback_hash
  353. return None
  354. def _get_processing_values(self):
  355. """ Return the values used to process the transaction.
  356. The values are returned as a dict containing entries with the following keys:
  357. - `provider_id`: The provider handling the transaction, as a `payment.provider` id.
  358. - `provider_code`: The code of the provider.
  359. - `reference`: The reference of the transaction.
  360. - `amount`: The rounded amount of the transaction.
  361. - `currency_id`: The currency of the transaction, as a `res.currency` id.
  362. - `partner_id`: The partner making the transaction, as a `res.partner` id.
  363. - Additional provider-specific entries.
  364. Note: `self.ensure_one()`
  365. :return: The processing values.
  366. :rtype: dict
  367. """
  368. self.ensure_one()
  369. processing_values = {
  370. 'provider_id': self.provider_id.id,
  371. 'provider_code': self.provider_code,
  372. 'reference': self.reference,
  373. 'amount': self.amount,
  374. 'currency_id': self.currency_id.id,
  375. 'partner_id': self.partner_id.id,
  376. }
  377. # Complete generic processing values with provider-specific values.
  378. processing_values.update(self._get_specific_processing_values(processing_values))
  379. _logger.info(
  380. "generic and provider-specific processing values for transaction with reference "
  381. "%(ref)s:\n%(values)s",
  382. {'ref': self.reference, 'values': pprint.pformat(processing_values)},
  383. )
  384. # Render the html form for the redirect flow if available.
  385. if self.operation in ('online_redirect', 'validation'):
  386. redirect_form_view = self.provider_id._get_redirect_form_view(
  387. is_validation=self.operation == 'validation'
  388. )
  389. if redirect_form_view: # Some provider don't need a redirect form.
  390. rendering_values = self._get_specific_rendering_values(processing_values)
  391. _logger.info(
  392. "provider-specific rendering values for transaction with reference "
  393. "%(ref)s:\n%(values)s",
  394. {'ref': self.reference, 'values': pprint.pformat(rendering_values)},
  395. )
  396. redirect_form_html = self.env['ir.qweb']._render(redirect_form_view.id, rendering_values)
  397. processing_values.update(redirect_form_html=redirect_form_html)
  398. return processing_values
  399. def _get_specific_processing_values(self, processing_values):
  400. """ Return a dict of provider-specific values used to process the transaction.
  401. For a provider to add its own processing values, it must overwrite this method and return a
  402. dict of provider-specific values based on the generic values returned by this method.
  403. Provider-specific values take precedence over those of the dict of generic processing
  404. values.
  405. :param dict processing_values: The generic processing values of the transaction.
  406. :return: The dict of provider-specific processing values.
  407. :rtype: dict
  408. """
  409. return dict()
  410. def _get_specific_rendering_values(self, processing_values):
  411. """ Return a dict of provider-specific values used to render the redirect form.
  412. For a provider to add its own rendering values, it must overwrite this method and return a
  413. dict of provider-specific values based on the processing values (provider-specific
  414. processing values included).
  415. :param dict processing_values: The processing values of the transaction.
  416. :return: The dict of provider-specific rendering values.
  417. :rtype: dict
  418. """
  419. return dict()
  420. def _send_payment_request(self):
  421. """ Request the provider handling the transaction to make the payment.
  422. This method is exclusively used to make payments by token, which correspond to both the
  423. `online_token` and the `offline` transaction's `operation` field.
  424. For a provider to support tokenization, it must override this method and make an API request
  425. to make a payment.
  426. Note: `self.ensure_one()`
  427. :return: None
  428. """
  429. self.ensure_one()
  430. self._ensure_provider_is_not_disabled()
  431. self._log_sent_message()
  432. def _send_refund_request(self, amount_to_refund=None):
  433. """ Request the provider handling the transaction to refund it.
  434. For a provider to support refunds, it must override this method and make an API request to
  435. make a refund.
  436. Note: `self.ensure_one()`
  437. :param float amount_to_refund: The amount to be refunded.
  438. :return: The refund transaction created to process the refund request.
  439. :rtype: recordset of `payment.transaction`
  440. """
  441. self.ensure_one()
  442. self._ensure_provider_is_not_disabled()
  443. refund_tx = self._create_refund_transaction(amount_to_refund=amount_to_refund)
  444. refund_tx._log_sent_message()
  445. return refund_tx
  446. def _create_refund_transaction(self, amount_to_refund=None, **custom_create_values):
  447. """ Create a new transaction with the operation `refund` and the current transaction as
  448. source transaction.
  449. :param float amount_to_refund: The strictly positive amount to refund, in the same currency
  450. as the source transaction.
  451. :return: The refund transaction.
  452. :rtype: recordset of `payment.transaction`
  453. """
  454. self.ensure_one()
  455. return self.create({
  456. 'provider_id': self.provider_id.id,
  457. 'reference': self._compute_reference(self.provider_code, prefix=f'R-{self.reference}'),
  458. 'amount': -(amount_to_refund or self.amount),
  459. 'currency_id': self.currency_id.id,
  460. 'token_id': self.token_id.id,
  461. 'operation': 'refund',
  462. 'source_transaction_id': self.id,
  463. 'partner_id': self.partner_id.id,
  464. **custom_create_values,
  465. })
  466. def _send_capture_request(self):
  467. """ Request the provider handling the transaction to capture the payment.
  468. For a provider to support authorization, it must override this method and make an API
  469. request to capture the payment.
  470. Note: `self.ensure_one()`
  471. :return: None
  472. """
  473. self.ensure_one()
  474. self._ensure_provider_is_not_disabled()
  475. def _send_void_request(self):
  476. """ Request the provider handling the transaction to void the payment.
  477. For a provider to support authorization, it must override this method and make an API
  478. request to void the payment.
  479. Note: `self.ensure_one()`
  480. :return: None
  481. """
  482. self.ensure_one()
  483. self._ensure_provider_is_not_disabled()
  484. def _ensure_provider_is_not_disabled(self):
  485. """ Ensure that the provider's state is not `disabled` before sending a request to its
  486. provider.
  487. :return: None
  488. :raise UserError: If the provider's state is `disabled`.
  489. """
  490. if self.provider_id.state == 'disabled':
  491. raise UserError(_(
  492. "Making a request to the provider is not possible because the provider is disabled."
  493. ))
  494. def _handle_notification_data(self, provider_code, notification_data):
  495. """ Match the transaction with the notification data, update its state and return it.
  496. :param str provider_code: The code of the provider handling the transaction.
  497. :param dict notification_data: The notification data sent by the provider.
  498. :return: The transaction.
  499. :rtype: recordset of `payment.transaction`
  500. """
  501. tx = self._get_tx_from_notification_data(provider_code, notification_data)
  502. tx._process_notification_data(notification_data)
  503. tx._execute_callback()
  504. return tx
  505. def _get_tx_from_notification_data(self, provider_code, notification_data):
  506. """ Find the transaction based on the notification data.
  507. For a provider to handle transaction processing, it must overwrite this method and return
  508. the transaction matching the notification data.
  509. :param str provider_code: The code of the provider handling the transaction.
  510. :param dict notification_data: The notification data sent by the provider.
  511. :return: The transaction, if found.
  512. :rtype: recordset of `payment.transaction`
  513. """
  514. return self
  515. def _process_notification_data(self, notification_data):
  516. """ Update the transaction state and the provider reference based on the notification data.
  517. This method should usually not be called directly. The correct method to call upon receiving
  518. notification data is :meth:`_handle_notification_data`.
  519. For a provider to handle transaction processing, it must overwrite this method and process
  520. the notification data.
  521. Note: `self.ensure_one()`
  522. :param dict notification_data: The notification data sent by the provider.
  523. :return: None
  524. """
  525. self.ensure_one()
  526. def _set_pending(self, state_message=None):
  527. """ Update the transactions' state to `pending`.
  528. :param str state_message: The reason for setting the transactions in the state `pending`.
  529. :return: The updated transactions.
  530. :rtype: recordset of `payment.transaction`
  531. """
  532. allowed_states = ('draft',)
  533. target_state = 'pending'
  534. txs_to_process = self._update_state(allowed_states, target_state, state_message)
  535. txs_to_process._log_received_message()
  536. return txs_to_process
  537. def _set_authorized(self, state_message=None):
  538. """ Update the transactions' state to `authorized`.
  539. :param str state_message: The reason for setting the transactions in the state `authorized`.
  540. :return: The updated transactions.
  541. :rtype: recordset of `payment.transaction`
  542. """
  543. allowed_states = ('draft', 'pending')
  544. target_state = 'authorized'
  545. txs_to_process = self._update_state(allowed_states, target_state, state_message)
  546. txs_to_process._log_received_message()
  547. return txs_to_process
  548. def _set_done(self, state_message=None):
  549. """ Update the transactions' state to `done`.
  550. :param str state_message: The reason for setting the transactions in the state `done`.
  551. :return: The updated transactions.
  552. :rtype: recordset of `payment.transaction`
  553. """
  554. allowed_states = ('draft', 'pending', 'authorized', 'error', 'cancel') # 'cancel' for Payulatam
  555. target_state = 'done'
  556. txs_to_process = self._update_state(allowed_states, target_state, state_message)
  557. txs_to_process._log_received_message()
  558. return txs_to_process
  559. def _set_canceled(self, state_message=None):
  560. """ Update the transactions' state to `cancel`.
  561. :param str state_message: The reason for setting the transactions in the state `cancel`.
  562. :return: The updated transactions.
  563. :rtype: recordset of `payment.transaction`
  564. """
  565. allowed_states = ('draft', 'pending', 'authorized', 'done') # 'done' for Authorize refunds.
  566. target_state = 'cancel'
  567. txs_to_process = self._update_state(allowed_states, target_state, state_message)
  568. # Cancel the existing payments.
  569. txs_to_process._log_received_message()
  570. return txs_to_process
  571. def _set_error(self, state_message):
  572. """ Update the transactions' state to `error`.
  573. :param str state_message: The reason for setting the transactions in the state `error`.
  574. :return: The updated transactions.
  575. :rtype: recordset of `payment.transaction`
  576. """
  577. allowed_states = ('draft', 'pending', 'authorized', 'done') # 'done' for Stripe refunds.
  578. target_state = 'error'
  579. txs_to_process = self._update_state(allowed_states, target_state, state_message)
  580. txs_to_process._log_received_message()
  581. return txs_to_process
  582. def _update_state(self, allowed_states, target_state, state_message):
  583. """ Update the transactions' state to the target state if the current state allows it.
  584. If the current state is the same as the target state, the transaction is skipped and a log
  585. with level INFO is created.
  586. :param tuple[str] allowed_states: The allowed source states for the target state.
  587. :param str target_state: The target state.
  588. :param str state_message: The message to set as `state_message`.
  589. :return: The recordset of transactions whose state was updated.
  590. :rtype: recordset of `payment.transaction`
  591. """
  592. def classify_by_state(transactions_):
  593. """ Classify the transactions according to their current state.
  594. For each transaction of the current recordset, if:
  595. - The state is an allowed state: the transaction is flagged as `to process`.
  596. - The state is equal to the target state: the transaction is flagged as `processed`.
  597. - The state matches none of above: the transaction is flagged as `in wrong state`.
  598. :param recordset transactions_: The transactions to classify, as a `payment.transaction`
  599. recordset.
  600. :return: A 3-items tuple of recordsets of classified transactions, in this order:
  601. transactions `to process`, `processed`, and `in wrong state`.
  602. :rtype: tuple(recordset)
  603. """
  604. txs_to_process_ = transactions_.filtered(lambda _tx: _tx.state in allowed_states)
  605. txs_already_processed_ = transactions_.filtered(lambda _tx: _tx.state == target_state)
  606. txs_wrong_state_ = transactions_ - txs_to_process_ - txs_already_processed_
  607. return txs_to_process_, txs_already_processed_, txs_wrong_state_
  608. txs_to_process, txs_already_processed, txs_wrong_state = classify_by_state(self)
  609. for tx in txs_already_processed:
  610. _logger.info(
  611. "tried to write on transaction with reference %s with the same value for the "
  612. "state: %s",
  613. tx.reference, tx.state,
  614. )
  615. for tx in txs_wrong_state:
  616. _logger.warning(
  617. "tried to write on transaction with reference %(ref)s with illegal value for the "
  618. "state (previous state: %(tx_state)s, target state: %(target_state)s, expected "
  619. "previous state to be in: %(allowed_states)s)",
  620. {
  621. 'ref': tx.reference,
  622. 'tx_state': tx.state,
  623. 'target_state': target_state,
  624. 'allowed_states': allowed_states,
  625. },
  626. )
  627. txs_to_process.write({
  628. 'state': target_state,
  629. 'state_message': state_message,
  630. 'last_state_change': fields.Datetime.now(),
  631. })
  632. return txs_to_process
  633. def _execute_callback(self):
  634. """ Execute the callbacks defined on the transactions.
  635. Callbacks that have already been executed are silently ignored. For example, the callback is
  636. called twice when a transaction is first authorized then confirmed.
  637. Only successful callbacks are marked as done. This allows callbacks to reschedule
  638. themselves, should the conditions be unmet in the present call.
  639. :return: None
  640. """
  641. for tx in self.filtered(lambda t: not t.sudo().callback_is_done):
  642. # Only use sudo to check, not to execute.
  643. tx_sudo = tx.sudo()
  644. model_sudo = tx_sudo.callback_model_id
  645. res_id = tx_sudo.callback_res_id
  646. method = tx_sudo.callback_method
  647. callback_hash = tx_sudo.callback_hash
  648. if not (model_sudo and res_id and method):
  649. continue # Skip transactions with unset (or not properly defined) callbacks.
  650. valid_callback_hash = self._generate_callback_hash(model_sudo.id, res_id, method)
  651. if not consteq(ustr(valid_callback_hash), callback_hash):
  652. _logger.warning(
  653. "invalid callback signature for transaction with reference %s", tx.reference
  654. )
  655. continue # Ignore tampered callbacks.
  656. record = self.env[model_sudo.model].browse(res_id).exists()
  657. if not record:
  658. _logger.warning(
  659. "invalid callback record %(model)s.%(record_id)s for transaction with "
  660. "reference %(ref)s",
  661. {
  662. 'model': model_sudo.model,
  663. 'record_id': res_id,
  664. 'ref': tx.reference,
  665. }
  666. )
  667. continue # Ignore invalidated callbacks.
  668. success = getattr(record, method)(tx) # Execute the callback.
  669. tx_sudo.callback_is_done = success or success is None # Missing returns are successful.
  670. #=== BUSINESS METHODS - POST-PROCESSING ===#
  671. def _get_post_processing_values(self):
  672. """ Return a dict of values used to display the status of the transaction.
  673. For a provider to handle transaction status display, it must override this method and
  674. return a dict of values. Provider-specific values take precedence over those of the dict of
  675. generic post-processing values.
  676. The returned dict contains the following entries:
  677. - `provider_code`: The code of the provider.
  678. - `reference`: The reference of the transaction.
  679. - `amount`: The rounded amount of the transaction.
  680. - `currency_id`: The currency of the transaction, as a `res.currency` id.
  681. - `state`: The transaction state: `draft`, `pending`, `authorized`, `done`, `cancel`, or
  682. `error`.
  683. - `state_message`: The information message about the state.
  684. - `operation`: The operation of the transaction.
  685. - `is_post_processed`: Whether the transaction has already been post-processed.
  686. - `landing_route`: The route the user is redirected to after the transaction.
  687. - Additional provider-specific entries.
  688. Note: `self.ensure_one()`
  689. :return: The dict of processing values.
  690. :rtype: dict
  691. """
  692. self.ensure_one()
  693. post_processing_values = {
  694. 'provider_code': self.provider_code,
  695. 'reference': self.reference,
  696. 'amount': self.amount,
  697. 'currency_code': self.currency_id.name,
  698. 'state': self.state,
  699. 'state_message': self.state_message,
  700. 'operation': self.operation,
  701. 'is_post_processed': self.is_post_processed,
  702. 'landing_route': self.landing_route,
  703. }
  704. _logger.debug(
  705. "post-processing values of transaction with reference %s for provider with id %s:\n%s",
  706. self.reference, self.provider_id.id, pprint.pformat(post_processing_values)
  707. ) # DEBUG level because this can get spammy with transactions in non-final states
  708. return post_processing_values
  709. def _cron_finalize_post_processing(self):
  710. """ Finalize the post-processing of recently done transactions not handled by the client.
  711. :return: None
  712. """
  713. txs_to_post_process = self
  714. if not txs_to_post_process:
  715. # Let the client post-process transactions so that they remain available in the portal
  716. client_handling_limit_date = datetime.now() - relativedelta.relativedelta(minutes=10)
  717. # Don't try forever to post-process a transaction that doesn't go through. Set the limit
  718. # to 4 days because some providers (PayPal) need that much for the payment verification.
  719. retry_limit_date = datetime.now() - relativedelta.relativedelta(days=4)
  720. # Retrieve all transactions matching the criteria for post-processing
  721. txs_to_post_process = self.search([
  722. ('state', '=', 'done'),
  723. ('is_post_processed', '=', False),
  724. '|', ('last_state_change', '<=', client_handling_limit_date),
  725. ('operation', '=', 'refund'),
  726. ('last_state_change', '>=', retry_limit_date),
  727. ])
  728. for tx in txs_to_post_process:
  729. try:
  730. tx._finalize_post_processing()
  731. self.env.cr.commit()
  732. except psycopg2.OperationalError:
  733. self.env.cr.rollback() # Rollback and try later.
  734. except Exception as e:
  735. _logger.exception(
  736. "encountered an error while post-processing transaction with reference %s:\n%s",
  737. tx.reference, e
  738. )
  739. self.env.cr.rollback()
  740. def _finalize_post_processing(self):
  741. """ Trigger the final post-processing tasks and mark the transactions as post-processed.
  742. :return: None
  743. """
  744. self.filtered(lambda tx: tx.operation != 'validation')._reconcile_after_done()
  745. self.is_post_processed = True
  746. def _reconcile_after_done(self):
  747. """ Perform compute-intensive operations on related documents.
  748. For a provider to handle transaction post-processing, it must overwrite this method and
  749. execute its compute-intensive operations on documents linked to confirmed transactions.
  750. :return: None
  751. """
  752. return
  753. #=== BUSINESS METHODS - LOGGING ===#
  754. def _log_sent_message(self):
  755. """ Log that the transactions have been initiated in the chatter of relevant documents.
  756. :return: None
  757. """
  758. for tx in self:
  759. message = tx._get_sent_message()
  760. tx._log_message_on_linked_documents(message)
  761. def _log_received_message(self):
  762. """ Log that the transactions have been received in the chatter of relevant documents.
  763. A transaction is 'received' when a payment status is received from the provider handling the
  764. transaction.
  765. :return: None
  766. """
  767. for tx in self:
  768. message = tx._get_received_message()
  769. tx._log_message_on_linked_documents(message)
  770. def _log_message_on_linked_documents(self, message):
  771. """ Log a message on the records linked to the transaction.
  772. For a module to implement payments and link documents to a transaction, it must override
  773. this method and call it, then log the message on documents linked to the transaction.
  774. Note: `self.ensure_one()`
  775. :param str message: The message to log.
  776. :return: None
  777. """
  778. self.ensure_one()
  779. #=== BUSINESS METHODS - GETTERS ===#
  780. def _get_sent_message(self):
  781. """ Return the message stating that the transaction has been requested.
  782. Note: `self.ensure_one()`
  783. :return: The 'transaction sent' message.
  784. :rtype: str
  785. """
  786. self.ensure_one()
  787. # Choose the message based on the payment flow.
  788. if self.operation in ('online_redirect', 'online_direct'):
  789. message = _(
  790. "A transaction with reference %(ref)s has been initiated (%(provider_name)s).",
  791. ref=self.reference, provider_name=self.provider_id.name
  792. )
  793. elif self.operation == 'refund':
  794. formatted_amount = format_amount(self.env, -self.amount, self.currency_id)
  795. message = _(
  796. "A refund request of %(amount)s has been sent. The payment will be created soon. "
  797. "Refund transaction reference: %(ref)s (%(provider_name)s).",
  798. amount=formatted_amount, ref=self.reference, provider_name=self.provider_id.name
  799. )
  800. elif self.operation in ('online_token', 'offline'):
  801. message = _(
  802. "A transaction with reference %(ref)s has been initiated using the payment method "
  803. "%(token)s (%(provider_name)s).",
  804. ref=self.reference,
  805. token=self.token_id._build_display_name(),
  806. provider_name=self.provider_id.name
  807. )
  808. else: # 'validation'
  809. message = _(
  810. "A transaction with reference %(ref)s has been initiated to save a new payment "
  811. "method (%(provider_name)s)",
  812. ref=self.reference,
  813. provider_name=self.provider_id.name,
  814. )
  815. return message
  816. def _get_received_message(self):
  817. """ Return the message stating that the transaction has been received by the provider.
  818. Note: `self.ensure_one()`
  819. :return: The 'transaction received' message.
  820. :rtype: str
  821. """
  822. self.ensure_one()
  823. formatted_amount = format_amount(self.env, self.amount, self.currency_id)
  824. if self.state == 'pending':
  825. message = _(
  826. ("The transaction with reference %(ref)s for %(amount)s "
  827. "is pending (%(provider_name)s)."),
  828. ref=self.reference,
  829. amount=formatted_amount,
  830. provider_name=self.provider_id.name
  831. )
  832. elif self.state == 'authorized':
  833. message = _(
  834. "The transaction with reference %(ref)s for %(amount)s has been authorized "
  835. "(%(provider_name)s).", ref=self.reference, amount=formatted_amount,
  836. provider_name=self.provider_id.name
  837. )
  838. elif self.state == 'done':
  839. message = _(
  840. "The transaction with reference %(ref)s for %(amount)s has been confirmed "
  841. "(%(provider_name)s).", ref=self.reference, amount=formatted_amount,
  842. provider_name=self.provider_id.name
  843. )
  844. elif self.state == 'error':
  845. message = _(
  846. "The transaction with reference %(ref)s for %(amount)s encountered an error"
  847. " (%(provider_name)s).",
  848. ref=self.reference, amount=formatted_amount, provider_name=self.provider_id.name
  849. )
  850. if self.state_message:
  851. message += "<br />" + _("Error: %s", self.state_message)
  852. else:
  853. message = _(
  854. ("The transaction with reference %(ref)s for %(amount)s is canceled "
  855. "(%(provider_name)s)."),
  856. ref=self.reference,
  857. amount=formatted_amount,
  858. provider_name=self.provider_id.name
  859. )
  860. if self.state_message:
  861. message += "<br />" + _("Reason: %s", self.state_message)
  862. return message
  863. def _get_last(self):
  864. """ Return the last transaction of the recordset.
  865. :return: The last transaction of the recordset, sorted by id.
  866. :rtype: recordset of `payment.transaction`
  867. """
  868. return self.filtered(lambda t: t.state != 'draft').sorted()[:1]