diff --git a/addons/pos_restaurant_stripe/__init__.py b/addons/pos_restaurant_stripe/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/pos_restaurant_stripe/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/pos_restaurant_stripe/__manifest__.py b/addons/pos_restaurant_stripe/__manifest__.py new file mode 100644 index 00000000000..1bbf38f2e0c --- /dev/null +++ b/addons/pos_restaurant_stripe/__manifest__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'POS Restaurant Stripe', + 'version': '1.0', + 'category': 'Point of Sale', + 'sequence': 6, + 'summary': 'Adds American style tipping to Stripe', + 'depends': ['pos_stripe', 'pos_restaurant', 'payment_stripe'], + 'auto_install': True, + 'assets': { + 'point_of_sale.assets': [ + 'pos_restaurant_stripe/static/**/*', + ], + }, + 'license': 'LGPL-3', +} diff --git a/addons/pos_restaurant_stripe/models/__init__.py b/addons/pos_restaurant_stripe/models/__init__.py new file mode 100644 index 00000000000..f3a2a5855c1 --- /dev/null +++ b/addons/pos_restaurant_stripe/models/__init__.py @@ -0,0 +1,5 @@ +# coding: utf-8 +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import pos_payment +from . import pos_order diff --git a/addons/pos_restaurant_stripe/models/pos_order.py b/addons/pos_restaurant_stripe/models/pos_order.py new file mode 100644 index 00000000000..8a338adcb70 --- /dev/null +++ b/addons/pos_restaurant_stripe/models/pos_order.py @@ -0,0 +1,17 @@ +# coding: utf-8 +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import models + + +class PosOrder(models.Model): + _inherit = 'pos.order' + + def set_no_tip(self): + """Capture the payment when no tip is set.""" + res = super(PosOrder, self).set_no_tip() + + for payment in self.payment_ids: + if payment.payment_method_id.use_payment_terminal == 'stripe': + payment.payment_method_id.stripe_capture_payment(payment.transaction_id) + + return res diff --git a/addons/pos_restaurant_stripe/models/pos_payment.py b/addons/pos_restaurant_stripe/models/pos_payment.py new file mode 100644 index 00000000000..cbf3dd42f5e --- /dev/null +++ b/addons/pos_restaurant_stripe/models/pos_payment.py @@ -0,0 +1,16 @@ +# coding: utf-8 +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import models + + +class PosPayment(models.Model): + _inherit = 'pos.payment' + + def _update_payment_line_for_tip(self, tip_amount): + """Capture the payment when a tip is set.""" + res = super(PosPayment, self)._update_payment_line_for_tip(tip_amount) + + if self.payment_method_id.use_payment_terminal == 'stripe': + self.payment_method_id.stripe_capture_payment(self.transaction_id, amount=self.amount) + + return res diff --git a/addons/pos_restaurant_stripe/static/src/js/payment_stripe.js b/addons/pos_restaurant_stripe/static/src/js/payment_stripe.js new file mode 100644 index 00000000000..f7152375f6e --- /dev/null +++ b/addons/pos_restaurant_stripe/static/src/js/payment_stripe.js @@ -0,0 +1,21 @@ +odoo.define('pos_restaurant_stripe.payment', function (require) { + "use strict"; + + var PaymentStripe = require('pos_stripe.payment'); + + PaymentStripe.include({ + captureAfterPayment: async function (processPayment, line) { + // Don't capture if the customer can tip, in that case we + // will capture later. + if (! this.canBeAdjusted(line.cid)) { + return this._super(...arguments); + } + }, + + canBeAdjusted: function (cid) { + var order = this.pos.get_order(); + var line = order.get_paymentline(cid); + return this.pos.config.set_tip_after_payment && line.payment_method.use_payment_terminal === "stripe"; + } + }); +}); diff --git a/addons/pos_stripe/models/pos_payment_method.py b/addons/pos_stripe/models/pos_payment_method.py index 415a9a25609..d6e6efeb13a 100644 --- a/addons/pos_stripe/models/pos_payment_method.py +++ b/addons/pos_stripe/models/pos_payment_method.py @@ -60,6 +60,10 @@ class PosPaymentMethod(models.Model): _logger.error("Unexpected stripe_connection_token response: %s", resp.status_code) raise UserError(_("Unexpected error between us and Stripe.")) + def _stripe_calculate_amount(self, amount): + currency = self.journal_id.currency_id or self.company_id.currency_id + return int(amount/currency.rounding) + def stripe_payment_intent(self, amount): if not self.env.user.has_group('point_of_sale.group_pos_user'): raise AccessError(_("Do not have access to fetch token from Stripe")) @@ -72,7 +76,7 @@ class PosPaymentMethod(models.Model): try: data = werkzeug.urls.url_encode({ "currency": currency.name, - "amount": int(amount/currency.rounding), + "amount": self._stripe_calculate_amount(amount), "payment_method_types[]": "card_present", "capture_method": "manual", }) @@ -88,15 +92,28 @@ class PosPaymentMethod(models.Model): raise UserError(_("Unexpected error between us and Stripe.")) @api.model - def stripe_capture_payment(self, paymentIntentId): + def stripe_capture_payment(self, paymentIntentId, amount=None): + """Captures the payment identified by paymentIntentId. + + :param paymentIntentId: the id of the payment to capture + :param amount: without this parameter the entire authorized + amount is captured. Specifying a larger amount allows + overcapturing to support tips. + """ if not self.env.user.has_group('point_of_sale.group_pos_user'): raise AccessError(_("Do not have access to fetch token from Stripe")) endpoint = ('https://api.stripe.com/v1/payment_intents/%s/capture') % \ (werkzeug.urls.url_quote(paymentIntentId)) + data = None + if amount is not None: + data = { + "amount_to_capture": self._stripe_calculate_amount(amount), + } + try: - resp = requests.post(endpoint, auth=(self.sudo()._get_stripe_secret_key(), ''), timeout=TIMEOUT) + resp = requests.post(endpoint, data=data, auth=(self.sudo()._get_stripe_secret_key(), ''), timeout=TIMEOUT) except requests.exceptions.RequestException: _logger.exception("Failed to call stripe_capture_payment endpoint") raise UserError(_("There are some issues between us and Stripe, try again later.")) diff --git a/addons/pos_stripe/static/src/js/payment_stripe.js b/addons/pos_stripe/static/src/js/payment_stripe.js index 54831afbd93..1c5ba674a04 100644 --- a/addons/pos_stripe/static/src/js/payment_stripe.js +++ b/addons/pos_stripe/static/src/js/payment_stripe.js @@ -104,20 +104,25 @@ let PaymentStripe = PaymentInterface.extend({ } else { line.set_payment_status('waitingCapture'); let processPayment = await this.terminal.processPayment(collectPaymentMethod.paymentIntent); + line.transaction_id = collectPaymentMethod.paymentIntent.id; if (processPayment.error) { this._showError(processPayment.error.message, processPayment.error.code); line.set_payment_status('retry'); return false; } else if (processPayment.paymentIntent) { line.set_payment_status('waitingCapture'); - let capturePayment = await this.capturePayment(processPayment.paymentIntent.id); + await this.captureAfterPayment(processPayment, line); line.set_payment_status('done'); - line.transaction_id = (capturePayment.id); return true; } } }, + captureAfterPayment: async function (processPayment, line) { + let capturePayment = await this.capturePayment(processPayment.paymentIntent.id); + line.transaction_id = capturePayment.id; + }, + capturePayment: function (paymentIntentId) { let self = this; return rpc.query({