[IMP] website_(portal_sale|sale|quote), payment: add authorization payment flow steps
This allows using a modern e-commerce flow where amounts are first authorized at checkout then captured when the picking is in fact shipped instead of immediately capturing the amount. To access this flow, a new selection value is available on the payment acquirer: "Authorize the amount and confirm the SO on acquirer confirmation". If this value is selected, s2s/form transactions should only require authorization to the acquirer; the user can then capture/void the payment manually from the Odoo backend transaction form view by simply using buttons.
This commit is contained in:
@@ -71,6 +71,7 @@ class PaymentAcquirer(models.Model):
|
||||
help="Make this payment acquirer available (Customer invoices, etc.)")
|
||||
auto_confirm = fields.Selection([
|
||||
('none', 'No automatic confirmation'),
|
||||
('authorize', 'Authorize the amount and confirm the SO on acquirer confirmation'),
|
||||
('confirm_so', 'Confirm the SO on acquirer confirmation'),
|
||||
('generate_and_pay_invoice', 'On acquirer confirmation, confirm the SO, generate the invoice and pay it')],
|
||||
string='Order Confirmation', default='confirm_so', required=True)
|
||||
@@ -374,6 +375,7 @@ class PaymentTransaction(models.Model):
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('pending', 'Pending'),
|
||||
('authorized', 'Authorized'),
|
||||
('done', 'Done'),
|
||||
('error', 'Error'),
|
||||
('cancel', 'Canceled')], 'Status',
|
||||
@@ -558,15 +560,40 @@ class PaymentTransaction(models.Model):
|
||||
if hasattr(self, custom_method_name):
|
||||
return getattr(self, custom_method_name)(**kwargs)
|
||||
|
||||
@api.multi
|
||||
def s2s_capture_transaction(self, **kwargs):
|
||||
custom_method_name = '%s_s2s_capture_transaction' % self.acquirer_id.provider
|
||||
if hasattr(self, custom_method_name):
|
||||
return getattr(self, custom_method_name)(**kwargs)
|
||||
|
||||
@api.multi
|
||||
def s2s_void_transaction(self, **kwargs):
|
||||
custom_method_name = '%s_s2s_void_transaction' % self.acquirer_id.provider
|
||||
if hasattr(self, custom_method_name):
|
||||
return getattr(self, custom_method_name)(**kwargs)
|
||||
|
||||
@api.multi
|
||||
def s2s_get_tx_status(self):
|
||||
""" Get the tx status. """
|
||||
invalid_param_method_name = '_%s_s2s_get_tx_status' % self.acquirer_id.provider
|
||||
if hasattr(self, invalid_param_method_name):
|
||||
return getattr(self, invalid_param_method_name)()
|
||||
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def action_capture(self):
|
||||
if any(self.mapped(lambda tx: tx.state != 'authorized')):
|
||||
raise ValidationError('Only transactions in the Authorized status can be captured.')
|
||||
for tx in self:
|
||||
tx.s2s_capture_transaction()
|
||||
|
||||
@api.multi
|
||||
def action_void(self):
|
||||
if any(self.mapped(lambda tx: tx.state != 'authorized')):
|
||||
raise ValidationError('Only transactions in the Authorized status can be voided.')
|
||||
for tx in self:
|
||||
tx.s2s_void_transaction()
|
||||
|
||||
|
||||
class PaymentToken(models.Model):
|
||||
_name = 'payment.token'
|
||||
|
||||
@@ -208,6 +208,8 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Payment Transactions">
|
||||
<header>
|
||||
<button type="object" name="action_capture" states="authorized" string="Capture Transaction" class="oe_highlight"/>
|
||||
<button type="object" name="action_void" states="authorized" string="Void Transaction"/>
|
||||
<field name="state" widget="statusbar" clickable="True"/>
|
||||
</header>
|
||||
<sheet>
|
||||
|
||||
@@ -52,6 +52,9 @@ class PaymentTransaction(models.Model):
|
||||
# verify SO/TX match, excluding tx.fees which are currently not included in SO
|
||||
amount_matches = float_compare(tx.amount, tx.sale_order_id.amount_total, 2) == 0
|
||||
if amount_matches:
|
||||
if tx.state == 'authorized' and tx.acquirer_id.auto_confirm == 'authorize':
|
||||
_logger.info('<%s> transaction authorized, auto-confirming order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
|
||||
tx.sale_order_id.with_context(send_email=True).action_confirm()
|
||||
if tx.state == 'done' and tx.acquirer_id.auto_confirm in ['confirm_so', 'generate_and_pay_invoice']:
|
||||
_logger.info('<%s> transaction completed, auto-confirming order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
|
||||
tx.sale_order_id.with_context(send_email=True).action_confirm()
|
||||
|
||||
@@ -271,6 +271,11 @@
|
||||
Your transaction is waiting confirmation.
|
||||
<span t-if='tx_post_msg' t-raw="tx_post_msg"/>
|
||||
</div>
|
||||
<div class="alert alert-warning alert-dismissable" t-if="tx_id and tx_state == 'authorized'">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
|
||||
Your payment has been authorized.
|
||||
<span t-if='tx_post_msg' t-raw="tx_post_msg"/>
|
||||
</div>
|
||||
<div class="alert alert-warning alert-dismissable" t-if="message==4 and quotation.state != 'sent'">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
|
||||
This order
|
||||
|
||||
@@ -748,7 +748,8 @@ class WebsiteSale(http.Controller):
|
||||
tx.partner_id == tx.sale_order_id.partner_id):
|
||||
try:
|
||||
s2s_result = tx.s2s_do_transaction()
|
||||
if not s2s_result or tx.state != 'done':
|
||||
valid_state = 'authorized' if tx.acquirer_id.auto_confirm == 'authorize' else 'done'
|
||||
if not s2s_result or tx.state != valid_state:
|
||||
return dict(success=False, error=_("Payment transaction failed (%s)") % tx.state_message)
|
||||
return dict(success=True, url='/shop/payment/validate')
|
||||
except Exception, e:
|
||||
@@ -884,7 +885,7 @@ class WebsiteSale(http.Controller):
|
||||
if not order or (order.amount_total and not tx):
|
||||
return request.redirect('/shop')
|
||||
|
||||
if (not order.amount_total and not tx) or tx.state in ['pending', 'done']:
|
||||
if (not order.amount_total and not tx) or tx.state in ['pending', 'done', 'authorized']:
|
||||
if (not order.amount_total and not tx):
|
||||
# Orders are confirmed by payment transactions, but there is none for free orders,
|
||||
# (e.g. free events), so confirm immediately
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='amount_total']" position="after">
|
||||
<field name="payment_acquirer_id" readonly="1" attrs="{'invisible': [('payment_acquirer_id', '=', False)]}" options='{"no_open":True}'/>
|
||||
<field name="payment_tx_id" readonly="1" attrs="{'invisible': [('payment_acquirer_id', '=', False)]}" groups="base.group_no_one"/>
|
||||
<field name="payment_tx_id" readonly="1" attrs="{'invisible': [('payment_acquirer_id', '=', False)]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -1535,6 +1535,9 @@
|
||||
<t t-if="tx_ids and state == 'done'">
|
||||
<p>Your payment has been received.</p>
|
||||
</t>
|
||||
<t t-if="tx_ids and state == 'authorized'">
|
||||
<p>Your payment has been authorized.</p>
|
||||
</t>
|
||||
<t t-if="tx_ids and state == 'cancel'">
|
||||
<p>The payment seems to have been canceled.</p>
|
||||
</t>
|
||||
@@ -1547,7 +1550,7 @@
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="tx_ids and state not in ['done', 'cancel', 'pending']">
|
||||
<t t-if="tx_ids and state not in ['done', 'cancel', 'pending', 'authorized']">
|
||||
<p>Your transaction is waiting confirmation.</p>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user