res_partner_bank.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. # -*- coding: utf-8 -*-
  2. import base64
  3. from collections import defaultdict
  4. import werkzeug
  5. import werkzeug.exceptions
  6. from odoo import _, api, fields, models
  7. from odoo.exceptions import UserError, ValidationError
  8. from odoo.tools.image import image_data_uri
  9. class ResPartnerBank(models.Model):
  10. _name = 'res.partner.bank'
  11. _inherit = ['res.partner.bank', 'mail.thread', 'mail.activity.mixin']
  12. journal_id = fields.One2many(
  13. 'account.journal', 'bank_account_id', domain=[('type', '=', 'bank')], string='Account Journal', readonly=True,
  14. help="The accounting journal corresponding to this bank account.")
  15. # Add tracking to the base fields
  16. bank_id = fields.Many2one(tracking=True)
  17. active = fields.Boolean(tracking=True)
  18. acc_number = fields.Char(tracking=True)
  19. acc_holder_name = fields.Char(tracking=True)
  20. partner_id = fields.Many2one(tracking=True)
  21. allow_out_payment = fields.Boolean(tracking=True)
  22. currency_id = fields.Many2one(tracking=True)
  23. @api.constrains('journal_id')
  24. def _check_journal_id(self):
  25. for bank in self:
  26. if len(bank.journal_id) > 1:
  27. raise ValidationError(_('A bank account can belong to only one journal.'))
  28. def _build_qr_code_vals(self, amount, free_communication, structured_communication, currency, debtor_partner, qr_method=None, silent_errors=True):
  29. """ Returns the QR-code vals needed to generate the QR-code report link to pay this account with the given parameters,
  30. or None if no QR-code could be generated.
  31. :param amount: The amount to be paid
  32. :param free_communication: Free communication to add to the payment when generating one with the QR-code
  33. :param structured_communication: Structured communication to add to the payment when generating one with the QR-code
  34. :param currency: The currency in which amount is expressed
  35. :param debtor_partner: The partner to which this QR-code is aimed (so the one who will have to pay)
  36. :param qr_method: The QR generation method to be used to make the QR-code. If None, the first one giving a result will be used.
  37. :param silent_errors: If true, forbids errors to be raised if some tested QR-code format can't be generated because of incorrect data.
  38. """
  39. if not self:
  40. return None
  41. self.ensure_one()
  42. if not currency:
  43. raise UserError(_("Currency must always be provided in order to generate a QR-code"))
  44. available_qr_methods = self.get_available_qr_methods_in_sequence()
  45. candidate_methods = qr_method and [(qr_method, dict(available_qr_methods)[qr_method])] or available_qr_methods
  46. for candidate_method, candidate_name in candidate_methods:
  47. if self._eligible_for_qr_code(candidate_method, debtor_partner, currency, not silent_errors):
  48. error_message = self._check_for_qr_code_errors(candidate_method, amount, currency, debtor_partner, free_communication, structured_communication)
  49. if not error_message:
  50. return {
  51. 'qr_method': candidate_method,
  52. 'amount': amount,
  53. 'currency': currency,
  54. 'debtor_partner': debtor_partner,
  55. 'free_communication': free_communication,
  56. 'structured_communication': structured_communication,
  57. }
  58. elif not silent_errors:
  59. error_header = _("The following error prevented '%s' QR-code to be generated though it was detected as eligible: ", candidate_name)
  60. raise UserError(error_header + error_message)
  61. return None
  62. def build_qr_code_url(self, amount, free_communication, structured_communication, currency, debtor_partner, qr_method=None, silent_errors=True):
  63. vals = self._build_qr_code_vals(amount, free_communication, structured_communication, currency, debtor_partner, qr_method, silent_errors)
  64. if vals:
  65. return self._get_qr_code_url(**vals)
  66. return None
  67. def build_qr_code_base64(self, amount, free_communication, structured_communication, currency, debtor_partner, qr_method=None, silent_errors=True):
  68. vals = self._build_qr_code_vals(amount, free_communication, structured_communication, currency, debtor_partner, qr_method, silent_errors)
  69. if vals:
  70. return self._get_qr_code_base64(**vals)
  71. return None
  72. def _get_qr_vals(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
  73. return None
  74. def _get_qr_code_generation_params(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
  75. raise NotImplementedError()
  76. def _get_qr_code_url(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
  77. """ Hook for extension, to support the different QR generation methods.
  78. This function uses the provided qr_method to try generation a QR-code for
  79. the given data. It it succeeds, it returns the report URL to make this
  80. QR-code; else None.
  81. :param qr_method: The QR generation method to be used to make the QR-code.
  82. :param amount: The amount to be paid
  83. :param currency: The currency in which amount is expressed
  84. :param debtor_partner: The partner to which this QR-code is aimed (so the one who will have to pay)
  85. :param free_communication: Free communication to add to the payment when generating one with the QR-code
  86. :param structured_communication: Structured communication to add to the payment when generating one with the QR-code
  87. """
  88. params = self._get_qr_code_generation_params(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
  89. return '/report/barcode/?' + werkzeug.urls.url_encode(params) if params else None
  90. def _get_qr_code_base64(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
  91. """ Hook for extension, to support the different QR generation methods.
  92. This function uses the provided qr_method to try generation a QR-code for
  93. the given data. It it succeeds, it returns QR code in base64 url; else None.
  94. :param qr_method: The QR generation method to be used to make the QR-code.
  95. :param amount: The amount to be paid
  96. :param currency: The currency in which amount is expressed
  97. :param debtor_partner: The partner to which this QR-code is aimed (so the one who will have to pay)
  98. :param free_communication: Free communication to add to the payment when generating one with the QR-code
  99. :param structured_communication: Structured communication to add to the payment when generating one with the QR-code
  100. """
  101. params = self._get_qr_code_generation_params(qr_method, amount, currency, debtor_partner, free_communication, structured_communication)
  102. if params:
  103. try:
  104. barcode = self.env['ir.actions.report'].barcode(**params)
  105. except (ValueError, AttributeError):
  106. raise werkzeug.exceptions.HTTPException(description='Cannot convert into barcode.')
  107. return image_data_uri(base64.b64encode(barcode))
  108. return None
  109. @api.model
  110. def _get_available_qr_methods(self):
  111. """ Returns the QR-code generation methods that are available on this db,
  112. in the form of a list of (code, name, sequence) elements, where
  113. 'code' is a unique string identifier, 'name' the name to display
  114. to the user to designate the method, and 'sequence' is a positive integer
  115. indicating the order in which those mehtods need to be checked, to avoid
  116. shadowing between them (lower sequence means more prioritary).
  117. """
  118. return []
  119. @api.model
  120. def get_available_qr_methods_in_sequence(self):
  121. """ Same as _get_available_qr_methods but without returning the sequence,
  122. and using it directly to order the returned list.
  123. """
  124. all_available = self._get_available_qr_methods()
  125. all_available.sort(key=lambda x: x[2])
  126. return [(code, name) for (code, name, sequence) in all_available]
  127. def _eligible_for_qr_code(self, qr_method, debtor_partner, currency, raises_error=True):
  128. """ Tells whether or not the criteria to apply QR-generation
  129. method qr_method are met for a payment on this account, in the
  130. given currency, by debtor_partner. This does not impeach generation errors,
  131. it only checks that this type of QR-code *should be* possible to generate.
  132. Consistency of the required field needs then to be checked by _check_for_qr_code_errors().
  133. """
  134. return False
  135. def _check_for_qr_code_errors(self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication):
  136. """ Checks the data before generating a QR-code for the specified qr_method
  137. (this method must have been checked for eligbility by _eligible_for_qr_code() first).
  138. Returns None if no error was found, or a string describing the first error encountered
  139. so that it can be reported to the user.
  140. """
  141. return None
  142. @api.model_create_multi
  143. def create(self, vals_list):
  144. # EXTENDS base res.partner.bank
  145. res = super().create(vals_list)
  146. for account in res:
  147. msg = _("Bank Account %s created", account._get_html_link(title=f"#{account.id}"))
  148. account.partner_id._message_log(body=msg)
  149. return res
  150. def write(self, vals):
  151. # EXTENDS base res.partner.bank
  152. # Track and log changes to partner_id, heavily inspired from account_move
  153. account_initial_values = defaultdict(dict)
  154. # Get all tracked fields (without related fields because these fields must be managed on their own model)
  155. tracking_fields = []
  156. for field_name in vals:
  157. field = self._fields[field_name]
  158. if not (hasattr(field, 'related') and field.related) and hasattr(field, 'tracking') and field.tracking:
  159. tracking_fields.append(field_name)
  160. fields_definition = self.env['res.partner.bank'].fields_get(tracking_fields)
  161. # Get initial values for each account
  162. for account in self:
  163. for field in tracking_fields:
  164. # Group initial values by partner_id
  165. account_initial_values[account][field] = account[field]
  166. res = super().write(vals)
  167. # Log changes to move lines on each move
  168. for account, initial_values in account_initial_values.items():
  169. tracking_value_ids = account._mail_track(fields_definition, initial_values)[1]
  170. if tracking_value_ids:
  171. msg = _("Bank Account %s updated", account._get_html_link(title=f"#{account.id}"))
  172. account.partner_id._message_log(body=msg, tracking_value_ids=tracking_value_ids)
  173. if 'partner_id' in initial_values: # notify previous partner as well
  174. initial_values['partner_id']._message_log(body=msg, tracking_value_ids=tracking_value_ids)
  175. return res
  176. def unlink(self):
  177. # EXTENDS base res.partner.bank
  178. for account in self:
  179. msg = _("Bank Account %s with number %s deleted", account._get_html_link(title=f"#{account.id}"), account.acc_number)
  180. account.partner_id._message_log(body=msg)
  181. return super().unlink()