1# coding: utf-8
2from collections import defaultdict
3import hashlib
4import hmac
5import logging
6from datetime import datetime
7from dateutil import relativedelta
8import pprint
9import psycopg2
10
11from odoo import api, exceptions, fields, models, _, SUPERUSER_ID
12from odoo.tools import consteq, float_round, image_process, ustr
13from odoo.exceptions import ValidationError
14from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
15from odoo.tools.misc import formatLang
16from odoo.http import request
17from odoo.osv import expression
18
19_logger = logging.getLogger(__name__)
20
21
22def _partner_format_address(address1=False, address2=False):
23    return ' '.join((address1 or '', address2 or '')).strip()
24
25
26def _partner_split_name(partner_name):
27    return [' '.join(partner_name.split()[:-1]), ' '.join(partner_name.split()[-1:])]
28
29
30def create_missing_journal_for_acquirers(cr, registry):
31    env = api.Environment(cr, SUPERUSER_ID, {})
32    env['payment.acquirer']._create_missing_journal_for_acquirers()
33
34
35class PaymentAcquirer(models.Model):
36    """ Acquirer Model. Each specific acquirer can extend the model by adding
37    its own fields, using the acquirer_name as a prefix for the new fields.
38    Using the required_if_provider='<name>' attribute on fields it is possible
39    to have required fields that depend on a specific acquirer.
40
41    Each acquirer has a link to an ir.ui.view record that is a template of
42    a button used to display the payment form. See examples in ``payment_ingenico``
43    and ``payment_paypal`` modules.
44
45    Methods that should be added in an acquirer-specific implementation:
46
47     - ``<name>_form_generate_values(self, reference, amount, currency,
48       partner_id=False, partner_values=None, tx_custom_values=None)``:
49       method that generates the values used to render the form button template.
50     - ``<name>_get_form_action_url(self):``: method that returns the url of
51       the button form. It is used for example in ecommerce application if you
52       want to post some data to the acquirer.
53     - ``<name>_compute_fees(self, amount, currency_id, country_id)``: computes
54       the fees of the acquirer, using generic fields defined on the acquirer
55       model (see fields definition).
56
57    Each acquirer should also define controllers to handle communication between
58    OpenERP and the acquirer. It generally consists in return urls given to the
59    button form and that the acquirer uses to send the customer back after the
60    transaction, with transaction details given as a POST request.
61    """
62    _name = 'payment.acquirer'
63    _description = 'Payment Acquirer'
64    _order = 'module_state, state, sequence, name'
65
66    def _valid_field_parameter(self, field, name):
67        return name == 'required_if_provider' or super()._valid_field_parameter(field, name)
68
69    def _get_default_view_template_id(self):
70        return self.env.ref('payment.default_acquirer_button', raise_if_not_found=False)
71
72    name = fields.Char('Name', required=True, translate=True)
73    color = fields.Integer('Color', compute='_compute_color', store=True)
74    display_as = fields.Char('Displayed as', translate=True, help="How the acquirer is displayed to the customers.")
75    description = fields.Html('Description')
76    sequence = fields.Integer('Sequence', default=10, help="Determine the display order")
77    provider = fields.Selection(
78        selection=[('manual', 'Custom Payment Form')], string='Provider',
79        default='manual', required=True)
80    company_id = fields.Many2one(
81        'res.company', 'Company',
82        default=lambda self: self.env.company.id, required=True)
83    view_template_id = fields.Many2one(
84        'ir.ui.view', 'Form Button Template',
85        default=_get_default_view_template_id,
86        help="This template renders the acquirer button with all necessary values.\n"
87        "It is rendered with qWeb with the following evaluation context:\n"
88        "tx_url: transaction URL to post the form\n"
89        "acquirer: payment.acquirer browse record\n"
90        "user: current user browse record\n"
91        "reference: the transaction reference number\n"
92        "currency: the transaction currency browse record\n"
93        "amount: the transaction amount, a float\n"
94        "partner: the buyer partner browse record, not necessarily set\n"
95        "partner_values: specific values about the buyer, for example coming from a shipping form\n"
96        "tx_values: transaction values\n"
97        "context: the current context dictionary")
98    registration_view_template_id = fields.Many2one(
99        'ir.ui.view', 'S2S Form Template', domain=[('type', '=', 'qweb')],
100        help="Template for method registration")
101    state = fields.Selection([
102        ('disabled', 'Disabled'),
103        ('enabled', 'Enabled'),
104        ('test', 'Test Mode')], required=True, default='disabled', copy=False,
105        help="""In test mode, a fake payment is processed through a test
106             payment interface. This mode is advised when setting up the
107             acquirer. Watch out, test and production modes require
108             different credentials.""")
109    capture_manually = fields.Boolean(string="Capture Amount Manually",
110        help="Capture the amount from Odoo, when the delivery is completed.")
111    journal_id = fields.Many2one(
112        'account.journal', 'Payment Journal', domain="[('type', 'in', ['bank', 'cash']), ('company_id', '=', company_id)]",
113        help="""Journal where the successful transactions will be posted""")
114    check_validity = fields.Boolean(string="Verify Card Validity",
115        help="""Trigger a transaction of 1 currency unit and its refund to check the validity of new credit cards entered in the customer portal.
116        Without this check, the validity will be verified at the very first transaction.""")
117    country_ids = fields.Many2many(
118        'res.country', 'payment_country_rel',
119        'payment_id', 'country_id', 'Countries',
120        help="This payment gateway is available for selected countries. If none is selected it is available for all countries.")
121
122    pre_msg = fields.Html(
123        'Help Message', translate=True,
124        help='Message displayed to explain and help the payment process.')
125    auth_msg = fields.Html(
126        'Authorize Message', translate=True,
127        default=lambda s: _('Your payment has been authorized.'),
128        help='Message displayed if payment is authorized.')
129    pending_msg = fields.Html(
130        'Pending Message', translate=True,
131        default=lambda s: _('Your payment has been successfully processed but is waiting for approval.'),
132        help='Message displayed, if order is in pending state after having done the payment process.')
133    done_msg = fields.Html(
134        'Done Message', translate=True,
135        default=lambda s: _('Your payment has been successfully processed. Thank you!'),
136        help='Message displayed, if order is done successfully after having done the payment process.')
137    cancel_msg = fields.Html(
138        'Cancel Message', translate=True,
139        default=lambda s: _('Your payment has been cancelled.'),
140        help='Message displayed, if order is cancel during the payment process.')
141    save_token = fields.Selection([
142        ('none', 'Never'),
143        ('ask', 'Let the customer decide'),
144        ('always', 'Always')],
145        string='Save Cards', default='none',
146        help="This option allows customers to save their credit card as a payment token and to reuse it for a later purchase. "
147             "If you manage subscriptions (recurring invoicing), you need it to automatically charge the customer when you "
148             "issue an invoice.")
149    token_implemented = fields.Boolean('Saving Card Data supported', compute='_compute_feature_support', search='_search_is_tokenized')
150    authorize_implemented = fields.Boolean('Authorize Mechanism Supported', compute='_compute_feature_support')
151    fees_implemented = fields.Boolean('Fees Computation Supported', compute='_compute_feature_support')
152    fees_active = fields.Boolean('Add Extra Fees')
153    fees_dom_fixed = fields.Float('Fixed domestic fees')
154    fees_dom_var = fields.Float('Variable domestic fees (in percents)')
155    fees_int_fixed = fields.Float('Fixed international fees')
156    fees_int_var = fields.Float('Variable international fees (in percents)')
157    qr_code = fields.Boolean('Enable QR Codes', help="Enable the use of QR-codes for payments made on this provider.")
158
159    # TDE FIXME: remove that brol
160    module_id = fields.Many2one('ir.module.module', string='Corresponding Module')
161    module_state = fields.Selection(string='Installation State', related='module_id.state', store=True)
162    module_to_buy = fields.Boolean(string='Odoo Enterprise Module', related='module_id.to_buy', readonly=True, store=False)
163
164    image_128 = fields.Image("Image", max_width=128, max_height=128)
165
166    payment_icon_ids = fields.Many2many('payment.icon', string='Supported Payment Icons')
167    payment_flow = fields.Selection(selection=[('form', 'Redirection to the acquirer website'),
168        ('s2s','Payment from Odoo')],
169        default='form', required=True, string='Payment Flow',
170        help="""Note: Subscriptions does not take this field in account, it uses server to server by default.""")
171    inbound_payment_method_ids = fields.Many2many('account.payment.method', related='journal_id.inbound_payment_method_ids', readonly=False)
172
173    @api.onchange('payment_flow')
174    def _onchange_payment_flow(self):
175        electronic = self.env.ref('payment.account_payment_method_electronic_in')
176        if self.token_implemented and self.payment_flow == 's2s':
177            if electronic not in self.inbound_payment_method_ids:
178                self.inbound_payment_method_ids = [(4, electronic.id)]
179        elif electronic in self.inbound_payment_method_ids:
180            self.inbound_payment_method_ids = [(2, electronic.id)]
181
182    @api.onchange('state')
183    def onchange_state(self):
184        """Disable dashboard display for test acquirer journal."""
185        self.journal_id.update({'show_on_dashboard': self.state == 'enabled'})
186
187    def _search_is_tokenized(self, operator, value):
188        tokenized = self._get_feature_support()['tokenize']
189        if (operator, value) in [('=', True), ('!=', False)]:
190            return [('provider', 'in', tokenized)]
191        return [('provider', 'not in', tokenized)]
192
193    @api.depends('provider')
194    def _compute_feature_support(self):
195        feature_support = self._get_feature_support()
196        for acquirer in self:
197            acquirer.fees_implemented = acquirer.provider in feature_support['fees']
198            acquirer.authorize_implemented = acquirer.provider in feature_support['authorize']
199            acquirer.token_implemented = acquirer.provider in feature_support['tokenize']
200
201    @api.depends('state', 'module_state')
202    def _compute_color(self):
203        for acquirer in self:
204            if acquirer.module_id and not acquirer.module_state == 'installed':
205                acquirer.color = 4  # blue
206            elif acquirer.state == 'disabled':
207                acquirer.color = 3  # yellow
208            elif acquirer.state == 'test':
209                acquirer.color = 2  # orange
210            elif acquirer.state == 'enabled':
211                acquirer.color = 7  # green
212
213    def _check_required_if_provider(self):
214        """ If the field has 'required_if_provider="<provider>"' attribute, then it
215        required if record.provider is <provider>. """
216        field_names = []
217        enabled_acquirers = self.filtered(lambda acq: acq.state in ['enabled', 'test'])
218        for k, f in self._fields.items():
219            provider = getattr(f, 'required_if_provider', None)
220            if provider and any(
221                acquirer.provider == provider and not acquirer[k]
222                for acquirer in enabled_acquirers
223            ):
224                ir_field = self.env['ir.model.fields']._get(self._name, k)
225                field_names.append(ir_field.field_description)
226        if field_names:
227            raise ValidationError(_("Required fields not filled: %s") % ", ".join(field_names))
228
229    def get_base_url(self):
230        self.ensure_one()
231        # priority is always given to url_root
232        # from the request
233        url = ''
234        if request:
235            url = request.httprequest.url_root
236
237        if not url and 'website_id' in self and self.website_id:
238            url = self.website_id._get_http_domain()
239
240        return url or self.env['ir.config_parameter'].sudo().get_param('web.base.url')
241
242    def _get_feature_support(self):
243        """Get advanced feature support by provider.
244
245        Each provider should add its technical in the corresponding
246        key for the following features:
247            * fees: support payment fees computations
248            * authorize: support authorizing payment (separates
249                         authorization and capture)
250            * tokenize: support saving payment data in a payment.tokenize
251                        object
252        """
253        return dict(authorize=[], tokenize=[], fees=[])
254
255    def _prepare_account_journal_vals(self):
256        '''Prepare the values to create the acquirer's journal.
257        :return: a dictionary to create a account.journal record.
258        '''
259        self.ensure_one()
260        account_vals = self.company_id.chart_template_id._prepare_transfer_account_for_direct_creation(self.name, self.company_id)
261        account = self.env['account.account'].create(account_vals)
262        inbound_payment_method_ids = []
263        if self.token_implemented and self.payment_flow == 's2s':
264            inbound_payment_method_ids.append((4, self.env.ref('payment.account_payment_method_electronic_in').id))
265        return {
266            'name': self.name,
267            'code': self.name.upper(),
268            'sequence': 999,
269            'type': 'bank',
270            'company_id': self.company_id.id,
271            'default_account_id': account.id,
272            # Show the journal on dashboard if the acquirer is published on the website.
273            'show_on_dashboard': self.state == 'enabled',
274            # Don't show payment methods in the backend.
275            'inbound_payment_method_ids': inbound_payment_method_ids,
276            'outbound_payment_method_ids': [],
277        }
278
279    def _get_acquirer_journal_domain(self):
280        """Returns a domain for finding a journal corresponding to an acquirer"""
281        self.ensure_one()
282        code_cutoff = self.env['account.journal']._fields['code'].size
283        return [
284            ('name', '=', self.name),
285            ('code', '=', self.name.upper()[:code_cutoff]),
286            ('company_id', '=', self.company_id.id),
287        ]
288
289    @api.model
290    def _create_missing_journal_for_acquirers(self, company=None):
291        '''Create the journal for active acquirers.
292        We want one journal per acquirer. However, we can't create them during the 'create' of the payment.acquirer
293        because every acquirers are defined on the 'payment' module but is active only when installing their own module
294        (e.g. payment_paypal for Paypal). We can't do that in such modules because we have no guarantee the chart template
295        is already installed.
296        '''
297        # Search for installed acquirers modules that have no journal for the current company.
298        # If this method is triggered by a post_init_hook, the module is 'to install'.
299        # If the trigger comes from the chart template wizard, the modules are already installed.
300        company = company or self.env.company
301        acquirers = self.env['payment.acquirer'].search([
302            ('module_state', 'in', ('to install', 'installed')),
303            ('journal_id', '=', False),
304            ('company_id', '=', company.id),
305        ])
306
307        # Here we will attempt to first create the journal since the most common case (first
308        # install) is to successfully to create the journal for the acquirer, in the case of a
309        # reinstall (least common case), the creation will fail because of a unique constraint
310        # violation, this is ok as we catch the error and then perform a search if need be
311        # and assign the existing journal to our reinstalled acquirer. It is better to ask for
312        # forgiveness than to ask for permission as this saves us the overhead of doing a select
313        # that would be useless in most cases.
314        Journal = journals = self.env['account.journal']
315        for acquirer in acquirers.filtered(lambda l: not l.journal_id and l.company_id.chart_template_id):
316            try:
317                with self.env.cr.savepoint():
318                    journal = Journal.create(acquirer._prepare_account_journal_vals())
319            except psycopg2.IntegrityError as e:
320                if e.pgcode == psycopg2.errorcodes.UNIQUE_VIOLATION:
321                    journal = Journal.search(acquirer._get_acquirer_journal_domain(), limit=1)
322                else:
323                    raise
324            acquirer.journal_id = journal
325            journals += journal
326        return journals
327
328    @api.model
329    def create(self, vals):
330        record = super(PaymentAcquirer, self).create(vals)
331        record._check_required_if_provider()
332        return record
333
334    def write(self, vals):
335        result = super(PaymentAcquirer, self).write(vals)
336        self._check_required_if_provider()
337        return result
338
339    def get_acquirer_extra_fees(self, amount, currency_id, country_id):
340        extra_fees = {
341            'currency_id': currency_id
342        }
343        acquirers = self.filtered(lambda x: x.fees_active)
344        for acq in acquirers:
345            custom_method_name = '%s_compute_fees' % acq.provider
346            if hasattr(acq, custom_method_name):
347                fees = getattr(acq, custom_method_name)(amount, currency_id, country_id)
348                extra_fees[acq] = fees
349        return extra_fees
350
351    def get_form_action_url(self):
352        """ Returns the form action URL, for form-based acquirer implementations. """
353        if hasattr(self, '%s_get_form_action_url' % self.provider):
354            return getattr(self, '%s_get_form_action_url' % self.provider)()
355        return False
356
357    def _get_available_payment_input(self, partner=None, company=None):
358        """ Generic (model) method that fetches available payment mechanisms
359        to use in all portal / eshop pages that want to use the payment form.
360
361        It contains
362
363         * acquirers: record set of both form and s2s acquirers;
364         * pms: record set of stored credit card data (aka payment.token)
365                connected to a given partner to allow customers to reuse them """
366        if not company:
367            company = self.env.company
368        if not partner:
369            partner = self.env.user.partner_id
370
371        domain = expression.AND([
372            ['&', ('state', 'in', ['enabled', 'test']), ('company_id', '=', company.id)],
373            ['|', ('country_ids', '=', False), ('country_ids', 'in', [partner.country_id.id])]
374        ])
375        active_acquirers = self.search(domain)
376        acquirers = active_acquirers.filtered(lambda acq: (acq.payment_flow == 'form' and acq.view_template_id) or
377                                                               (acq.payment_flow == 's2s' and acq.registration_view_template_id))
378        return {
379            'acquirers': acquirers,
380            'pms': self.env['payment.token'].search([
381                ('partner_id', '=', partner.id),
382                ('acquirer_id', 'in', acquirers.ids)]),
383        }
384
385    def render(self, reference, amount, currency_id, partner_id=False, values=None):
386        """ Renders the form template of the given acquirer as a qWeb template.
387        :param string reference: the transaction reference
388        :param float amount: the amount the buyer has to pay
389        :param currency_id: currency id
390        :param dict partner_id: optional partner_id to fill values
391        :param dict values: a dictionary of values for the transction that is
392        given to the acquirer-specific method generating the form values
393
394        All templates will receive:
395
396         - acquirer: the payment.acquirer browse record
397         - user: the current user browse record
398         - currency_id: id of the transaction currency
399         - amount: amount of the transaction
400         - reference: reference of the transaction
401         - partner_*: partner-related values
402         - partner: optional partner browse record
403         - 'feedback_url': feedback URL, controler that manage answer of the acquirer (without base url) -> FIXME
404         - 'return_url': URL for coming back after payment validation (wihout base url) -> FIXME
405         - 'cancel_url': URL if the client cancels the payment -> FIXME
406         - 'error_url': URL if there is an issue with the payment -> FIXME
407         - context: Odoo context
408
409        """
410        if values is None:
411            values = {}
412
413        if not self.view_template_id:
414            return None
415
416        values.setdefault('return_url', '/payment/process')
417        # reference and amount
418        values.setdefault('reference', reference)
419        amount = float_round(amount, 2)
420        values.setdefault('amount', amount)
421
422        # currency id
423        currency_id = values.setdefault('currency_id', currency_id)
424        if currency_id:
425            currency = self.env['res.currency'].browse(currency_id)
426        else:
427            currency = self.env.company.currency_id
428        values['currency'] = currency
429
430        # Fill partner_* using values['partner_id'] or partner_id argument
431        partner_id = values.get('partner_id', partner_id)
432        billing_partner_id = values.get('billing_partner_id', partner_id)
433        if partner_id:
434            partner = self.env['res.partner'].browse(partner_id)
435            if partner_id != billing_partner_id:
436                billing_partner = self.env['res.partner'].browse(billing_partner_id)
437            else:
438                billing_partner = partner
439            values.update({
440                'partner': partner,
441                'partner_id': partner_id,
442                'partner_name': partner.name,
443                'partner_lang': partner.lang,
444                'partner_email': partner.email,
445                'partner_zip': partner.zip,
446                'partner_city': partner.city,
447                'partner_address': _partner_format_address(partner.street, partner.street2),
448                'partner_country_id': partner.country_id.id or self.env['res.company']._company_default_get().country_id.id,
449                'partner_country': partner.country_id,
450                'partner_phone': partner.phone,
451                'partner_state': partner.state_id,
452                'billing_partner': billing_partner,
453                'billing_partner_id': billing_partner_id,
454                'billing_partner_name': billing_partner.name,
455                'billing_partner_commercial_company_name': billing_partner.commercial_company_name,
456                'billing_partner_lang': billing_partner.lang,
457                'billing_partner_email': billing_partner.email,
458                'billing_partner_zip': billing_partner.zip,
459                'billing_partner_city': billing_partner.city,
460                'billing_partner_address': _partner_format_address(billing_partner.street, billing_partner.street2),
461                'billing_partner_country_id': billing_partner.country_id.id,
462                'billing_partner_country': billing_partner.country_id,
463                'billing_partner_phone': billing_partner.phone,
464                'billing_partner_state': billing_partner.state_id,
465            })
466        if values.get('partner_name'):
467            values.update({
468                'partner_first_name': _partner_split_name(values.get('partner_name'))[0],
469                'partner_last_name': _partner_split_name(values.get('partner_name'))[1],
470            })
471        if values.get('billing_partner_name'):
472            values.update({
473                'billing_partner_first_name': _partner_split_name(values.get('billing_partner_name'))[0],
474                'billing_partner_last_name': _partner_split_name(values.get('billing_partner_name'))[1],
475            })
476
477        # Fix address, country fields
478        if not values.get('partner_address'):
479            values['address'] = _partner_format_address(values.get('partner_street', ''), values.get('partner_street2', ''))
480        if not values.get('partner_country') and values.get('partner_country_id'):
481            values['country'] = self.env['res.country'].browse(values.get('partner_country_id'))
482        if not values.get('billing_partner_address'):
483            values['billing_address'] = _partner_format_address(values.get('billing_partner_street', ''), values.get('billing_partner_street2', ''))
484        if not values.get('billing_partner_country') and values.get('billing_partner_country_id'):
485            values['billing_country'] = self.env['res.country'].browse(values.get('billing_partner_country_id'))
486
487        # compute fees
488        fees_method_name = '%s_compute_fees' % self.provider
489        if hasattr(self, fees_method_name):
490            fees = getattr(self, fees_method_name)(values['amount'], values['currency_id'], values.get('partner_country_id'))
491            values['fees'] = float_round(fees, 2)
492
493        # call <name>_form_generate_values to update the tx dict with acqurier specific values
494        cust_method_name = '%s_form_generate_values' % (self.provider)
495        if hasattr(self, cust_method_name):
496            method = getattr(self, cust_method_name)
497            values = method(values)
498
499        values.update({
500            'tx_url': self._context.get('tx_url', self.get_form_action_url()),
501            'submit_class': self._context.get('submit_class', 'btn btn-link'),
502            'submit_txt': self._context.get('submit_txt'),
503            'acquirer': self,
504            'user': self.env.user,
505            'context': self._context,
506            'type': values.get('type') or 'form',
507        })
508
509        _logger.info('payment.acquirer.render: <%s> values rendered for form payment:\n%s', self.provider, pprint.pformat(values))
510        return self.view_template_id._render(values, engine='ir.qweb')
511
512    def get_s2s_form_xml_id(self):
513        if self.registration_view_template_id:
514            model_data = self.env['ir.model.data'].search([('model', '=', 'ir.ui.view'), ('res_id', '=', self.registration_view_template_id.id)])
515            return ('%s.%s') % (model_data.module, model_data.name)
516        return False
517
518    def s2s_process(self, data):
519        cust_method_name = '%s_s2s_form_process' % (self.provider)
520        if not self.s2s_validate(data):
521            return False
522        if hasattr(self, cust_method_name):
523            # As this method may be called in JSON and overridden in various addons
524            # let us raise interesting errors before having stranges crashes
525            if not data.get('partner_id'):
526                raise ValueError(_('Missing partner reference when trying to create a new payment token'))
527            method = getattr(self, cust_method_name)
528            return method(data)
529        return True
530
531    def s2s_validate(self, data):
532        cust_method_name = '%s_s2s_form_validate' % (self.provider)
533        if hasattr(self, cust_method_name):
534            method = getattr(self, cust_method_name)
535            return method(data)
536        return True
537
538    def button_immediate_install(self):
539        # TDE FIXME: remove that brol
540        if self.module_id and self.module_state != 'installed':
541            self.module_id.button_immediate_install()
542            return {
543                'type': 'ir.actions.client',
544                'tag': 'reload',
545            }
546
547class PaymentIcon(models.Model):
548    _name = 'payment.icon'
549    _description = 'Payment Icon'
550
551    name = fields.Char(string='Name')
552    acquirer_ids = fields.Many2many('payment.acquirer', string="Acquirers", help="List of Acquirers supporting this payment icon.")
553    image = fields.Binary(
554        "Image", help="This field holds the image used for this payment icon, limited to 1024x1024px")
555
556    image_payment_form = fields.Binary(
557        "Image displayed on the payment form", attachment=True)
558
559    @api.model_create_multi
560    def create(self, vals_list):
561        for vals in vals_list:
562            if 'image' in vals:
563                image = ustr(vals['image'] or '').encode('utf-8')
564                vals['image_payment_form'] = image_process(image, size=(45,30))
565                vals['image'] = image_process(image, size=(64,64))
566        return super(PaymentIcon, self).create(vals_list)
567
568    def write(self, vals):
569        if 'image' in vals:
570            image = ustr(vals['image'] or '').encode('utf-8')
571            vals['image_payment_form'] = image_process(image, size=(45,30))
572            vals['image'] = image_process(image, size=(64,64))
573        return super(PaymentIcon, self).write(vals)
574
575class PaymentTransaction(models.Model):
576    """ Transaction Model. Each specific acquirer can extend the model by adding
577    its own fields.
578
579    Methods that can be added in an acquirer-specific implementation:
580
581     - ``<name>_create``: method receiving values used when creating a new
582       transaction and that returns a dictionary that will update those values.
583       This method can be used to tweak some transaction values.
584
585    Methods defined for convention, depending on your controllers:
586
587     - ``<name>_form_feedback(self, data)``: method that handles the data coming
588       from the acquirer after the transaction. It will generally receives data
589       posted by the acquirer after the transaction.
590    """
591    _name = 'payment.transaction'
592    _description = 'Payment Transaction'
593    _order = 'id desc'
594    _rec_name = 'reference'
595
596    @api.model
597    def _lang_get(self):
598        return self.env['res.lang'].get_installed()
599
600    @api.model
601    def _get_default_partner_country_id(self):
602        return self.env.company.country_id.id
603
604    date = fields.Datetime('Validation Date', readonly=True)
605    acquirer_id = fields.Many2one('payment.acquirer', string='Acquirer', readonly=True, required=True)
606    provider = fields.Selection(string='Provider', related='acquirer_id.provider', readonly=True)
607    type = fields.Selection([
608        ('validation', 'Validation of the bank card'),
609        ('server2server', 'Server To Server'),
610        ('form', 'Form'),
611        ('form_save', 'Form with tokenization')], 'Type',
612        default='form', required=True, readonly=True)
613    state = fields.Selection([
614        ('draft', 'Draft'),
615        ('pending', 'Pending'),
616        ('authorized', 'Authorized'),
617        ('done', 'Done'),
618        ('cancel', 'Canceled'),
619        ('error', 'Error'),],
620        string='Status', copy=False, default='draft', required=True, readonly=True)
621    state_message = fields.Text(string='Message', readonly=True,
622                                help='Field used to store error and/or validation messages for information')
623    amount = fields.Monetary(string='Amount', currency_field='currency_id', required=True, readonly=True)
624    fees = fields.Monetary(string='Fees', currency_field='currency_id', readonly=True,
625                           help='Fees amount; set by the system because depends on the acquirer')
626    currency_id = fields.Many2one('res.currency', 'Currency', required=True, readonly=True)
627    reference = fields.Char(string='Reference', required=True, readonly=True, index=True,
628                            help='Internal reference of the TX')
629    acquirer_reference = fields.Char(string='Acquirer Reference', readonly=True, help='Reference of the TX as stored in the acquirer database')
630    # duplicate partner / transaction data to store the values at transaction time
631    partner_id = fields.Many2one('res.partner', 'Customer')
632    partner_name = fields.Char('Partner Name')
633    partner_lang = fields.Selection(_lang_get, 'Language', default=lambda self: self.env.lang)
634    partner_email = fields.Char('Email')
635    partner_zip = fields.Char('Zip')
636    partner_address = fields.Char('Address')
637    partner_city = fields.Char('City')
638    partner_country_id = fields.Many2one('res.country', 'Country', default=_get_default_partner_country_id, required=True)
639    partner_phone = fields.Char('Phone')
640    html_3ds = fields.Char('3D Secure HTML')
641
642    callback_model_id = fields.Many2one('ir.model', 'Callback Document Model', groups="base.group_system")
643    callback_res_id = fields.Integer('Callback Document ID', groups="base.group_system")
644    callback_method = fields.Char('Callback Method', groups="base.group_system")
645    callback_hash = fields.Char('Callback Hash', groups="base.group_system")
646
647    # Fields used for user redirection & payment post processing
648    return_url = fields.Char('Return URL after payment')
649    is_processed = fields.Boolean('Has the payment been post processed', default=False)
650
651    # Fields used for payment.transaction traceability.
652
653    payment_token_id = fields.Many2one('payment.token', 'Payment Token', readonly=True,
654                                       domain="[('acquirer_id', '=', acquirer_id)]")
655
656    payment_id = fields.Many2one('account.payment', string='Payment', readonly=True)
657    invoice_ids = fields.Many2many('account.move', 'account_invoice_transaction_rel', 'transaction_id', 'invoice_id',
658        string='Invoices', copy=False, readonly=True,
659        domain=[('move_type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund'))])
660    invoice_ids_nbr = fields.Integer(compute='_compute_invoice_ids_nbr', string='# of Invoices')
661
662    _sql_constraints = [
663        ('reference_uniq', 'unique(reference)', 'Reference must be unique!'),
664    ]
665
666    @api.depends('invoice_ids')
667    def _compute_invoice_ids_nbr(self):
668        for trans in self:
669            trans.invoice_ids_nbr = len(trans.invoice_ids)
670
671    def _create_payment(self, add_payment_vals={}):
672        ''' Create an account.payment record for the current payment.transaction.
673        If the transaction is linked to some invoices, the reconciliation will be done automatically.
674        :param add_payment_vals:    Optional additional values to be passed to the account.payment.create method.
675        :return:                    An account.payment record.
676        '''
677        self.ensure_one()
678
679        payment_vals = {
680            'amount': self.amount,
681            'payment_type': 'inbound' if self.amount > 0 else 'outbound',
682            'currency_id': self.currency_id.id,
683            'partner_id': self.partner_id.commercial_partner_id.id,
684            'partner_type': 'customer',
685            'journal_id': self.acquirer_id.journal_id.id,
686            'company_id': self.acquirer_id.company_id.id,
687            'payment_method_id': self.env.ref('payment.account_payment_method_electronic_in').id,
688            'payment_token_id': self.payment_token_id and self.payment_token_id.id or None,
689            'payment_transaction_id': self.id,
690            'ref': self.reference,
691            **add_payment_vals,
692        }
693        payment = self.env['account.payment'].create(payment_vals)
694        payment.action_post()
695
696        # Track the payment to make a one2one.
697        self.payment_id = payment
698
699        if self.invoice_ids:
700            self.invoice_ids.filtered(lambda move: move.state == 'draft')._post()
701
702            (payment.line_ids + self.invoice_ids.line_ids)\
703                .filtered(lambda line: line.account_id == payment.destination_account_id and not line.reconciled)\
704                .reconcile()
705
706        return payment
707
708    def get_last_transaction(self):
709        transactions = self.filtered(lambda t: t.state != 'draft')
710        return transactions and transactions[0] or transactions
711
712    def _get_processing_info(self):
713        """ Extensible method for providers if they need specific fields/info regarding a tx in the payment processing page. """
714        return dict()
715
716    def _get_payment_transaction_sent_message(self):
717        self.ensure_one()
718        if self.payment_token_id:
719            message = _('A transaction %s with %s initiated using %s credit card.')
720            message_vals = (self.reference, self.acquirer_id.name, self.payment_token_id.name)
721        elif self.provider in ('manual', 'transfer'):
722            message = _('The customer has selected %s to pay this document.')
723            message_vals = (self.acquirer_id.name)
724        else:
725            message = _('A transaction %s with %s initiated.')
726            message_vals = (self.reference, self.acquirer_id.name)
727        if self.provider not in ('manual', 'transfer'):
728            message += ' ' + _('Waiting for payment confirmation...')
729        return message % message_vals
730
731    def _get_payment_transaction_received_message(self):
732        self.ensure_one()
733        amount = formatLang(self.env, self.amount, currency_obj=self.currency_id)
734        message_vals = [self.reference, self.acquirer_id.name, amount]
735        if self.state == 'pending':
736            message = _('The transaction %s with %s for %s is pending.')
737        elif self.state == 'authorized':
738            message = _('The transaction %s with %s for %s has been authorized. Waiting for capture...')
739        elif self.state == 'done':
740            message = _('The transaction %s with %s for %s has been confirmed. The related payment is posted: %s')
741            message_vals.append(self.payment_id._get_payment_chatter_link())
742        elif self.state == 'cancel' and self.state_message:
743            message = _('The transaction %s with %s for %s has been cancelled with the following message: %s')
744            message_vals.append(self.state_message)
745        elif self.state == 'error' and self.state_message:
746            message = _('The transaction %s with %s for %s has return failed with the following error message: %s')
747            message_vals.append(self.state_message)
748        else:
749            message = _('The transaction %s with %s for %s has been cancelled.')
750        return message % tuple(message_vals)
751
752    def _log_payment_transaction_sent(self):
753        '''Log the message saying the transaction has been sent to the remote server to be
754        processed by the acquirer.
755        '''
756        for trans in self:
757            post_message = trans._get_payment_transaction_sent_message()
758            for inv in trans.invoice_ids:
759                inv.message_post(body=post_message)
760
761    def _log_payment_transaction_received(self):
762        '''Log the message saying a response has been received from the remote server and some
763        additional informations like the old/new state, the reference of the payment... etc.
764        :param old_state:       The state of the transaction before the response.
765        :param add_messages:    Optional additional messages to log like the capture status.
766        '''
767        for trans in self.filtered(lambda t: t.provider not in ('manual', 'transfer')):
768            post_message = trans._get_payment_transaction_received_message()
769            for inv in trans.invoice_ids:
770                inv.message_post(body=post_message)
771
772    def _filter_transaction_state(self, allowed_states, target_state):
773        """Divide a set of transactions according to their state.
774
775        :param tuple(string) allowed_states: tuple of allowed states for the target state (strings)
776        :param string target_state: target state for the filtering
777        :return: tuple of transactions divided by their state, in that order
778                    tx_to_process: tx that were in the allowed states
779                    tx_already_processed: tx that were already in the target state
780                    tx_wrong_state: tx that were not in the allowed state for the transition
781        :rtype: tuple(recordset)
782        """
783        tx_to_process = self.filtered(lambda tx: tx.state in allowed_states)
784        tx_already_processed = self.filtered(lambda tx: tx.state == target_state)
785        tx_wrong_state = self -tx_to_process - tx_already_processed
786        return (tx_to_process, tx_already_processed, tx_wrong_state)
787
788    def _set_transaction_pending(self):
789        '''Move the transaction to the pending state(e.g. Wire Transfer).'''
790        allowed_states = ('draft',)
791        target_state = 'pending'
792        (tx_to_process, tx_already_processed, tx_wrong_state) = self._filter_transaction_state(allowed_states, target_state)
793        for tx in tx_already_processed:
794            _logger.info('Trying to write the same state twice on tx (ref: %s, state: %s' % (tx.reference, tx.state))
795        for tx in tx_wrong_state:
796            _logger.warning('Processed tx with abnormal state (ref: %s, target state: %s, previous state %s, expected previous states: %s)' % (tx.reference, target_state, tx.state, allowed_states))
797
798        tx_to_process.write({
799            'state': target_state,
800            'date': fields.Datetime.now(),
801            'state_message': '',
802        })
803        tx_to_process._log_payment_transaction_received()
804
805    def _set_transaction_authorized(self):
806        '''Move the transaction to the authorized state(e.g. Authorize).'''
807        allowed_states = ('draft', 'pending')
808        target_state = 'authorized'
809        (tx_to_process, tx_already_processed, tx_wrong_state) = self._filter_transaction_state(allowed_states, target_state)
810        for tx in tx_already_processed:
811            _logger.info('Trying to write the same state twice on tx (ref: %s, state: %s' % (tx.reference, tx.state))
812        for tx in tx_wrong_state:
813            _logger.warning('Processed tx with abnormal state (ref: %s, target state: %s, previous state %s, expected previous states: %s)' % (tx.reference, target_state, tx.state, allowed_states))
814        tx_to_process.write({
815            'state': target_state,
816            'date': fields.Datetime.now(),
817            'state_message': '',
818        })
819        tx_to_process._log_payment_transaction_received()
820
821    def _set_transaction_done(self):
822        '''Move the transaction's payment to the done state(e.g. Paypal).'''
823        allowed_states = ('draft', 'authorized', 'pending', 'error')
824        target_state = 'done'
825        (tx_to_process, tx_already_processed, tx_wrong_state) = self._filter_transaction_state(allowed_states, target_state)
826        for tx in tx_already_processed:
827            _logger.info('Trying to write the same state twice on tx (ref: %s, state: %s' % (tx.reference, tx.state))
828        for tx in tx_wrong_state:
829            _logger.warning('Processed tx with abnormal state (ref: %s, target state: %s, previous state %s, expected previous states: %s)' % (tx.reference, target_state, tx.state, allowed_states))
830
831        tx_to_process.write({
832            'state': target_state,
833            'date': fields.Datetime.now(),
834            'state_message': '',
835        })
836
837    def _reconcile_after_transaction_done(self):
838        # Validate invoices automatically upon the transaction is posted.
839        invoices = self.mapped('invoice_ids').filtered(lambda inv: inv.state == 'draft')
840        invoices._post()
841
842        # Create & Post the payments.
843        for trans in self:
844            if trans.payment_id:
845                continue
846
847            trans._create_payment()
848
849    def _set_transaction_cancel(self):
850        '''Move the transaction's payment to the cancel state(e.g. Paypal).'''
851        allowed_states = ('draft', 'authorized')
852        target_state = 'cancel'
853        (tx_to_process, tx_already_processed, tx_wrong_state) = self._filter_transaction_state(allowed_states, target_state)
854        for tx in tx_already_processed:
855            _logger.info('Trying to write the same state twice on tx (ref: %s, state: %s' % (tx.reference, tx.state))
856        for tx in tx_wrong_state:
857            _logger.warning('Processed tx with abnormal state (ref: %s, target state: %s, previous state %s, expected previous states: %s)' % (tx.reference, target_state, tx.state, allowed_states))
858
859        # Cancel the existing payments.
860        tx_to_process.mapped('payment_id').action_cancel()
861
862        tx_to_process.write({'state': target_state, 'date': fields.Datetime.now()})
863        tx_to_process._log_payment_transaction_received()
864
865    def _set_transaction_error(self, msg):
866        '''Move the transaction to the error state (Third party returning error e.g. Paypal).'''
867        allowed_states = ('draft', 'authorized', 'pending')
868        target_state = 'error'
869        (tx_to_process, tx_already_processed, tx_wrong_state) = self._filter_transaction_state(allowed_states, target_state)
870        for tx in tx_already_processed:
871            _logger.info('Trying to write the same state twice on tx (ref: %s, state: %s' % (tx.reference, tx.state))
872        for tx in tx_wrong_state:
873            _logger.warning('Processed tx with abnormal state (ref: %s, target state: %s, previous state %s, expected previous states: %s)' % (tx.reference, target_state, tx.state, allowed_states))
874
875        tx_to_process.write({
876            'state': target_state,
877            'date': fields.Datetime.now(),
878            'state_message': msg,
879        })
880        tx_to_process._log_payment_transaction_received()
881
882    def _post_process_after_done(self):
883        self._reconcile_after_transaction_done()
884        self._log_payment_transaction_received()
885        self.write({'is_processed': True})
886        return True
887
888    def _cron_post_process_after_done(self):
889        if not self:
890            ten_minutes_ago = datetime.now() - relativedelta.relativedelta(minutes=10)
891            # we don't want to forever try to process a transaction that doesn't go through
892            retry_limit_date = datetime.now() - relativedelta.relativedelta(days=2)
893            # we retrieve all the payment tx that need to be post processed
894            self = self.search([('state', '=', 'done'),
895                                ('is_processed', '=', False),
896                                ('date', '<=', ten_minutes_ago),
897                                ('date', '>=', retry_limit_date),
898                            ])
899        for tx in self:
900            try:
901                tx._post_process_after_done()
902                self.env.cr.commit()
903            except Exception as e:
904                _logger.exception("Transaction post processing failed")
905                self.env.cr.rollback()
906
907    @api.model
908    def _compute_reference_prefix(self, values):
909        if values and values.get('invoice_ids'):
910            invoices = self.new({'invoice_ids': values['invoice_ids']}).invoice_ids
911            return ','.join(invoices.mapped('name'))
912        return None
913
914    @api.model
915    def _compute_reference(self, values=None, prefix=None):
916        '''Compute a unique reference for the transaction.
917        If prefix:
918            prefix-\d+
919        If some invoices:
920            <inv_number_0>.number,<inv_number_1>,...,<inv_number_n>-x
921        If some sale orders:
922            <so_name_0>.number,<so_name_1>,...,<so_name_n>-x
923        Else:
924            tx-\d+
925        :param values: values used to create a new transaction.
926        :param prefix: custom transaction prefix.
927        :return: A unique reference for the transaction.
928        '''
929        if not prefix:
930            prefix = self._compute_reference_prefix(values)
931            if not prefix:
932                prefix = 'tx'
933
934        # Fetch the last reference
935        # E.g. If the last reference is SO42-5, this query will return '-5'
936        self._cr.execute('''
937                SELECT CAST(SUBSTRING(reference FROM '-\d+$') AS INTEGER) AS suffix
938                FROM payment_transaction WHERE reference LIKE %s ORDER BY suffix
939            ''', [prefix + '-%'])
940        query_res = self._cr.fetchone()
941        if query_res:
942            # Increment the last reference by one
943            suffix = '%s' % (-query_res[0] + 1)
944        else:
945            # Start a new indexing from 1
946            suffix = '1'
947
948        return '%s-%s' % (prefix, suffix)
949
950    def action_view_invoices(self):
951        action = {
952            'name': _('Invoices'),
953            'type': 'ir.actions.act_window',
954            'res_model': 'account.move',
955            'target': 'current',
956        }
957        invoice_ids = self.invoice_ids.ids
958        if len(invoice_ids) == 1:
959            invoice = invoice_ids[0]
960            action['res_id'] = invoice
961            action['view_mode'] = 'form'
962            form_view = [(self.env.ref('account.view_move_form').id, 'form')]
963            if 'views' in action:
964                action['views'] = form_view + [(state,view) for state,view in action['views'] if view != 'form']
965            else:
966                action['views'] = form_view
967        else:
968            action['view_mode'] = 'tree,form'
969            action['domain'] = [('id', 'in', invoice_ids)]
970        return action
971
972    @api.constrains('state', 'acquirer_id')
973    def _check_authorize_state(self):
974        failed_tx = self.filtered(lambda tx: tx.state == 'authorized' and tx.acquirer_id.provider not in self.env['payment.acquirer']._get_feature_support()['authorize'])
975        if failed_tx:
976            raise exceptions.ValidationError(_('The %s payment acquirers are not allowed to manual capture mode!', failed_tx.mapped('acquirer_id.name')))
977
978    @api.model
979    def create(self, values):
980        # call custom create method if defined
981        acquirer = self.env['payment.acquirer'].browse(values['acquirer_id'])
982        if values.get('partner_id'):
983            partner = self.env['res.partner'].browse(values['partner_id'])
984
985            values.update({
986                'partner_name': partner.name,
987                'partner_lang': partner.lang or self.env.user.lang,
988                'partner_email': partner.email,
989                'partner_zip': partner.zip,
990                'partner_address': _partner_format_address(partner.street or '', partner.street2 or ''),
991                'partner_city': partner.city,
992                'partner_country_id': partner.country_id.id or self._get_default_partner_country_id(),
993                'partner_phone': partner.phone,
994            })
995
996        # compute fees
997        custom_method_name = '%s_compute_fees' % acquirer.provider
998        if hasattr(acquirer, custom_method_name):
999            fees = getattr(acquirer, custom_method_name)(
1000                values.get('amount', 0.0), values.get('currency_id'), values.get('partner_country_id', self._get_default_partner_country_id()))
1001            values['fees'] = fees
1002
1003        # custom create
1004        custom_method_name = '%s_create' % acquirer.provider
1005        if hasattr(self, custom_method_name):
1006            values.update(getattr(self, custom_method_name)(values))
1007
1008        if not values.get('reference'):
1009            values['reference'] = self._compute_reference(values=values)
1010
1011        # Default value of reference is
1012        tx = super(PaymentTransaction, self).create(values)
1013
1014        # Generate callback hash if it is configured on the tx; avoid generating unnecessary stuff
1015        # (limited sudo env for checking callback presence, must work for manual transactions too)
1016        tx_sudo = tx.sudo()
1017        if tx_sudo.callback_model_id and tx_sudo.callback_res_id and tx_sudo.callback_method:
1018            tx.write({'callback_hash': tx._generate_callback_hash()})
1019
1020        return tx
1021
1022    def _generate_callback_hash(self):
1023        self.ensure_one()
1024        secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
1025        token = '%s%s%s' % (self.callback_model_id.model,
1026                            self.callback_res_id,
1027                            self.sudo().callback_method)
1028        return hmac.new(secret.encode('utf-8'), token.encode('utf-8'), hashlib.sha256).hexdigest()
1029
1030    # --------------------------------------------------
1031    # FORM RELATED METHODS
1032    # --------------------------------------------------
1033
1034    @api.model
1035    def form_feedback(self, data, acquirer_name):
1036        invalid_parameters, tx = None, None
1037
1038        tx_find_method_name = '_%s_form_get_tx_from_data' % acquirer_name
1039        if hasattr(self, tx_find_method_name):
1040            tx = getattr(self, tx_find_method_name)(data)
1041
1042        # TDE TODO: form_get_invalid_parameters from model to multi
1043        invalid_param_method_name = '_%s_form_get_invalid_parameters' % acquirer_name
1044        if hasattr(self, invalid_param_method_name):
1045            invalid_parameters = getattr(tx, invalid_param_method_name)(data)
1046
1047        if invalid_parameters:
1048            _error_message = '%s: incorrect tx data:\n' % (acquirer_name)
1049            for item in invalid_parameters:
1050                _error_message += '\t%s: received %s instead of %s\n' % (item[0], item[1], item[2])
1051            _logger.error(_error_message)
1052            return False
1053
1054        # TDE TODO: form_validate from model to multi
1055        feedback_method_name = '_%s_form_validate' % acquirer_name
1056        if hasattr(self, feedback_method_name):
1057            return getattr(tx, feedback_method_name)(data)
1058
1059        return True
1060
1061    # --------------------------------------------------
1062    # SERVER2SERVER RELATED METHODS
1063    # --------------------------------------------------
1064
1065    def s2s_do_transaction(self, **kwargs):
1066        custom_method_name = '%s_s2s_do_transaction' % self.acquirer_id.provider
1067        for trans in self:
1068            trans._log_payment_transaction_sent()
1069            if hasattr(trans, custom_method_name):
1070                return getattr(trans, custom_method_name)(**kwargs)
1071
1072    def s2s_do_refund(self, **kwargs):
1073        custom_method_name = '%s_s2s_do_refund' % self.acquirer_id.provider
1074        if hasattr(self, custom_method_name):
1075            return getattr(self, custom_method_name)(**kwargs)
1076
1077    def s2s_capture_transaction(self, **kwargs):
1078        custom_method_name = '%s_s2s_capture_transaction' % self.acquirer_id.provider
1079        if hasattr(self, custom_method_name):
1080            return getattr(self, custom_method_name)(**kwargs)
1081
1082    def s2s_void_transaction(self, **kwargs):
1083        custom_method_name = '%s_s2s_void_transaction' % self.acquirer_id.provider
1084        if hasattr(self, custom_method_name):
1085            return getattr(self, custom_method_name)(**kwargs)
1086
1087    def s2s_get_tx_status(self):
1088        """ Get the tx status. """
1089        invalid_param_method_name = '_%s_s2s_get_tx_status' % self.acquirer_id.provider
1090        if hasattr(self, invalid_param_method_name):
1091            return getattr(self, invalid_param_method_name)()
1092        return True
1093
1094    def execute_callback(self):
1095        res = None
1096        for transaction in self:
1097            # limited sudo env, only for checking callback presence, not for running it!
1098            # manual transactions have no callback, and can pass without being run by admin user
1099            tx_sudo = transaction.sudo()
1100            if not (tx_sudo.callback_model_id and tx_sudo.callback_res_id and tx_sudo.callback_method):
1101                continue
1102
1103            valid_token = transaction._generate_callback_hash()
1104            if not consteq(ustr(valid_token), transaction.callback_hash):
1105                _logger.warning("Invalid callback signature for transaction %d" % (transaction.id))
1106                continue
1107
1108            record = self.env[transaction.callback_model_id.model].browse(transaction.callback_res_id).exists()
1109            if record:
1110                res = getattr(record, transaction.callback_method)(transaction)
1111            else:
1112                _logger.warning("Did not found record %s.%s for callback of transaction %d" % (transaction.callback_model_id.model, transaction.callback_res_id, transaction.id))
1113        return res
1114
1115    def action_capture(self):
1116        if any(t.state != 'authorized' for t in self):
1117            raise ValidationError(_('Only transactions having the authorized status can be captured.'))
1118        for tx in self:
1119            tx.s2s_capture_transaction()
1120
1121    def action_void(self):
1122        if any(t.state != 'authorized' for t in self):
1123            raise ValidationError(_('Only transactions having the capture status can be voided.'))
1124        for tx in self:
1125            tx.s2s_void_transaction()
1126
1127
1128class PaymentToken(models.Model):
1129    _name = 'payment.token'
1130    _order = 'partner_id, id desc'
1131    _description = 'Payment Token'
1132
1133    name = fields.Char('Name', help='Name of the payment token')
1134    short_name = fields.Char('Short name', compute='_compute_short_name')
1135    partner_id = fields.Many2one('res.partner', 'Partner', required=True)
1136    acquirer_id = fields.Many2one('payment.acquirer', 'Acquirer Account', required=True)
1137    company_id = fields.Many2one(related='acquirer_id.company_id', store=True, index=True)
1138    acquirer_ref = fields.Char('Acquirer Ref.', required=True)
1139    active = fields.Boolean('Active', default=True)
1140    payment_ids = fields.One2many('payment.transaction', 'payment_token_id', 'Payment Transactions')
1141    verified = fields.Boolean(string='Verified', default=False)
1142
1143    @api.model
1144    def create(self, values):
1145        # call custom create method if defined
1146        if values.get('acquirer_id'):
1147            acquirer = self.env['payment.acquirer'].browse(values['acquirer_id'])
1148
1149            # custom create
1150            custom_method_name = '%s_create' % acquirer.provider
1151            if hasattr(self, custom_method_name):
1152                values.update(getattr(self, custom_method_name)(values))
1153                # remove all non-model fields used by (provider)_create method to avoid warning
1154                fields_wl = set(self._fields) & set(values)
1155                values = {field: values[field] for field in fields_wl}
1156        return super(PaymentToken, self).create(values)
1157    """
1158        @TBE: stolen shamelessly from there https://www.paypal.com/us/selfhelp/article/why-is-there-a-$1.95-charge-on-my-card-statement-faq554
1159        Most of them are ~1.50€s
1160    """
1161    VALIDATION_AMOUNTS = {
1162        'CAD': 2.45,
1163        'EUR': 1.50,
1164        'GBP': 1.00,
1165        'JPY': 200,
1166        'AUD': 2.00,
1167        'NZD': 3.00,
1168        'CHF': 3.00,
1169        'HKD': 15.00,
1170        'SEK': 15.00,
1171        'DKK': 12.50,
1172        'PLN': 6.50,
1173        'NOK': 15.00,
1174        'HUF': 400.00,
1175        'CZK': 50.00,
1176        'BRL': 4.00,
1177        'MYR': 10.00,
1178        'MXN': 20.00,
1179        'ILS': 8.00,
1180        'PHP': 100.00,
1181        'TWD': 70.00,
1182        'THB': 70.00
1183        }
1184
1185    @api.model
1186    def validate(self, **kwargs):
1187        """
1188            This method allow to verify if this payment method is valid or not.
1189            It does this by withdrawing a certain amount and then refund it right after.
1190        """
1191        currency = self.partner_id.currency_id
1192
1193        if self.VALIDATION_AMOUNTS.get(currency.name):
1194            amount = self.VALIDATION_AMOUNTS.get(currency.name)
1195        else:
1196            # If we don't find the user's currency, then we set the currency to EUR and the amount to 1€50.
1197            currency = self.env['res.currency'].search([('name', '=', 'EUR')])
1198            amount = 1.5
1199
1200        if len(currency) != 1:
1201            _logger.error("Error 'EUR' currency not found for payment method validation!")
1202            return False
1203
1204        reference = "VALIDATION-%s-%s" % (self.id, datetime.now().strftime('%y%m%d_%H%M%S'))
1205        tx = self.env['payment.transaction'].sudo().create({
1206            'amount': amount,
1207            'acquirer_id': self.acquirer_id.id,
1208            'type': 'validation',
1209            'currency_id': currency.id,
1210            'reference': reference,
1211            'payment_token_id': self.id,
1212            'partner_id': self.partner_id.id,
1213            'partner_country_id': self.partner_id.country_id.id,
1214            'state_message': _('This Transaction was automatically processed & refunded in order to validate a new credit card.'),
1215        })
1216
1217        kwargs.update({'3d_secure': True})
1218        tx.s2s_do_transaction(**kwargs)
1219
1220        # if 3D secure is called, then we do not refund right now
1221        if not tx.html_3ds:
1222            tx.s2s_do_refund()
1223
1224        return tx
1225
1226    @api.depends('name')
1227    def _compute_short_name(self):
1228        for token in self:
1229            token.short_name = token.name.replace('XXXXXXXXXXXX', '***')
1230
1231    def get_linked_records(self):
1232        """ This method returns a dict containing all the records linked to the payment.token (e.g Subscriptions),
1233            the key is the id of the payment.token and the value is an array that must follow the scheme below.
1234
1235            {
1236                token_id: [
1237                    'description': The model description (e.g 'Sale Subscription'),
1238                    'id': The id of the record,
1239                    'name': The name of the record,
1240                    'url': The url to access to this record.
1241                ]
1242            }
1243        """
1244        return {r.id:[] for r in self}
1245