[ADD] pos_restaurant_stripe: support adding tip after payment
This is analogous to pos_restaurant_adyen and adds support for the two American tipping flows: - customer tips immediately themselves after paying using the TipScreen - waiter adds tips end-of-day using paper receipts using the TicketScreen To allow for tipping an optional amount= parameter was added to stripe_capture_payment and the transaction_id on the paymentline was saved a bit sooner so it can be used in the backend in set_no_tip and _update_payment_line_for_tip. Stripe docs: https://stripe.com/docs/terminal/features/collecting-tips/on-receipt task-2979312 closes odoo/odoo#99973 Signed-off-by: Masereel Pierre <pim@odoo.com>
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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";
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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."))
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user