changes
This commit is contained in:
@@ -15,12 +15,15 @@
|
||||
'report/poynt_receipt_report.xml',
|
||||
'report/poynt_receipt_templates.xml',
|
||||
|
||||
'data/poynt_surcharge_product.xml',
|
||||
|
||||
'views/payment_provider_views.xml',
|
||||
'views/payment_transaction_views.xml',
|
||||
'views/payment_poynt_templates.xml',
|
||||
'views/poynt_terminal_views.xml',
|
||||
'views/account_move_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'wizard/poynt_payment_wizard_views.xml',
|
||||
'wizard/poynt_refund_wizard_views.xml',
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
||||
from . import portal
|
||||
|
||||
@@ -19,6 +19,25 @@ from odoo.addons.fusion_poynt import utils as poynt_utils
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _detect_card_brand(card_number):
|
||||
"""Detect the card brand from the card number using BIN prefixes."""
|
||||
num = (card_number or '').replace(' ', '')
|
||||
if len(num) < 2:
|
||||
return 'other'
|
||||
if num[:2] in ('34', '37'):
|
||||
return 'amex'
|
||||
if num[0] == '4':
|
||||
return 'visa'
|
||||
prefix2 = int(num[:2])
|
||||
if 51 <= prefix2 <= 55:
|
||||
return 'mastercard'
|
||||
if len(num) >= 4:
|
||||
prefix4 = int(num[:4])
|
||||
if 2221 <= prefix4 <= 2720:
|
||||
return 'mastercard'
|
||||
return 'other'
|
||||
|
||||
|
||||
class PoyntController(http.Controller):
|
||||
_return_url = '/payment/poynt/return'
|
||||
_webhook_url = '/payment/poynt/webhook'
|
||||
@@ -344,6 +363,81 @@ class PoyntController(http.Controller):
|
||||
|
||||
return request.redirect('/odoo/settings')
|
||||
|
||||
# === SURCHARGE HELPER === #
|
||||
|
||||
def _apply_portal_surcharge(self, tx_sudo, card_type):
|
||||
"""Apply credit card surcharge to the linked invoice if enabled.
|
||||
|
||||
Detects the card brand from the number if card_type is not provided,
|
||||
adds a surcharge line to the invoice, and updates the transaction
|
||||
amount to include the fee.
|
||||
|
||||
:param tx_sudo: The sudo payment.transaction record.
|
||||
:param str card_type: The card brand (visa, mastercard, amex, other).
|
||||
:return: The surcharge fee amount, or 0 if not applied.
|
||||
:rtype: float
|
||||
"""
|
||||
ICP = request.env['ir.config_parameter'].sudo()
|
||||
if ICP.get_param('fusion_poynt.surcharge_enabled', 'False') != 'True':
|
||||
return 0.0
|
||||
|
||||
if not card_type:
|
||||
card_type = 'other'
|
||||
|
||||
rate_key = {
|
||||
'visa': 'fusion_poynt.surcharge_visa_rate',
|
||||
'mastercard': 'fusion_poynt.surcharge_mastercard_rate',
|
||||
'amex': 'fusion_poynt.surcharge_amex_rate',
|
||||
'debit': 'fusion_poynt.surcharge_debit_rate',
|
||||
}.get(card_type, 'fusion_poynt.surcharge_other_rate')
|
||||
|
||||
rate = float(ICP.get_param(rate_key, '0') or 0)
|
||||
if rate <= 0:
|
||||
return 0.0
|
||||
|
||||
invoices = tx_sudo.invoice_ids
|
||||
if not invoices:
|
||||
base_amount = tx_sudo.amount
|
||||
else:
|
||||
base_amount = sum(invoices.mapped('amount_residual'))
|
||||
|
||||
fee_amount = round(base_amount * rate / 100.0, 2)
|
||||
if fee_amount <= 0:
|
||||
return 0.0
|
||||
|
||||
product_id = int(ICP.get_param('fusion_poynt.surcharge_product_id', '0') or 0)
|
||||
product = request.env['product.product'].sudo().browse(product_id).exists()
|
||||
if not product:
|
||||
product = request.env.ref(
|
||||
'fusion_poynt.product_cc_processing_fee', raise_if_not_found=False,
|
||||
)
|
||||
if not product:
|
||||
_logger.warning("Surcharge product not configured; skipping surcharge")
|
||||
return 0.0
|
||||
|
||||
for invoice in invoices.sudo():
|
||||
was_posted = invoice.state == 'posted'
|
||||
if was_posted:
|
||||
invoice.button_draft()
|
||||
|
||||
description = "Credit Card Processing Fee (%.2f%% surcharge)" % rate
|
||||
invoice.write({
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'product_id': product.id,
|
||||
'name': description,
|
||||
'quantity': 1,
|
||||
'price_unit': fee_amount,
|
||||
'tax_ids': [(5, 0, 0)],
|
||||
})],
|
||||
})
|
||||
|
||||
if was_posted:
|
||||
invoice.action_post()
|
||||
|
||||
new_amount = tx_sudo.amount + fee_amount
|
||||
tx_sudo.write({'amount': new_amount})
|
||||
return fee_amount
|
||||
|
||||
# === JSON-RPC ROUTES (called from frontend JS) === #
|
||||
|
||||
@http.route('/payment/poynt/terminals', type='jsonrpc', auth='public')
|
||||
@@ -372,7 +466,8 @@ class PoyntController(http.Controller):
|
||||
@http.route('/payment/poynt/process_card', type='jsonrpc', auth='public')
|
||||
def poynt_process_card(self, reference=None, poynt_order_id=None,
|
||||
card_number=None, exp_month=None, exp_year=None,
|
||||
cvv=None, cardholder_name=None, **kwargs):
|
||||
cvv=None, cardholder_name=None, card_type=None,
|
||||
**kwargs):
|
||||
"""Process a card payment through Poynt Cloud API.
|
||||
|
||||
The frontend sends card details which are passed to Poynt for
|
||||
@@ -393,6 +488,11 @@ class PoyntController(http.Controller):
|
||||
return {'error': 'Transaction not found.'}
|
||||
|
||||
try:
|
||||
if not card_type and card_number:
|
||||
card_type = _detect_card_brand(card_number)
|
||||
|
||||
surcharge_fee = self._apply_portal_surcharge(tx_sudo, card_type)
|
||||
|
||||
funding_source = {
|
||||
'type': 'CREDIT_DEBIT',
|
||||
'card': {
|
||||
@@ -469,7 +569,7 @@ class PoyntController(http.Controller):
|
||||
|
||||
@http.route('/payment/poynt/send_to_terminal', type='jsonrpc', auth='public')
|
||||
def poynt_send_to_terminal(self, reference=None, terminal_id=None,
|
||||
poynt_order_id=None, **kwargs):
|
||||
poynt_order_id=None, card_type=None, **kwargs):
|
||||
"""Send a payment request to a Poynt terminal device.
|
||||
|
||||
:return: Dict with success status or error message.
|
||||
@@ -491,6 +591,8 @@ class PoyntController(http.Controller):
|
||||
return {'error': 'Terminal not found.'}
|
||||
|
||||
try:
|
||||
surcharge_fee = self._apply_portal_surcharge(tx_sudo, card_type or 'other')
|
||||
|
||||
result = terminal.action_send_payment_to_terminal(
|
||||
amount=tx_sudo.amount,
|
||||
currency=tx_sudo.currency_id,
|
||||
|
||||
60
fusion_poynt/controllers/portal.py
Normal file
60
fusion_poynt/controllers/portal.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
from odoo.addons.sale.controllers.portal import CustomerPortal
|
||||
|
||||
|
||||
class PoyntCustomerPortal(CustomerPortal):
|
||||
|
||||
@http.route()
|
||||
def portal_order_page(
|
||||
self,
|
||||
order_id,
|
||||
report_type=None,
|
||||
access_token=None,
|
||||
message=False,
|
||||
download=False,
|
||||
payment_amount=None,
|
||||
amount_selection=None,
|
||||
**kw
|
||||
):
|
||||
"""Auto-inject payment_amount for confirmed orders with outstanding balance.
|
||||
|
||||
For confirmed sale orders (state == 'sale') that haven't been fully
|
||||
paid, this automatically sets payment_amount to the remaining balance
|
||||
so that the standard portal "Pay Now" button appears without requiring
|
||||
a separate payment link URL.
|
||||
|
||||
Rental orders are excluded -- their payment flow is managed by
|
||||
fusion_rental.
|
||||
"""
|
||||
if payment_amount is None:
|
||||
try:
|
||||
order_sudo = self._document_check_access(
|
||||
'sale.order', order_id, access_token=access_token,
|
||||
)
|
||||
except Exception:
|
||||
order_sudo = None
|
||||
|
||||
if order_sudo:
|
||||
is_rental = getattr(order_sudo, 'is_rental_order', False)
|
||||
if (
|
||||
order_sudo.state == 'sale'
|
||||
and not is_rental
|
||||
and order_sudo.amount_total > 0
|
||||
and order_sudo.amount_paid < order_sudo.amount_total
|
||||
):
|
||||
payment_amount = order_sudo.amount_total - order_sudo.amount_paid
|
||||
|
||||
return super().portal_order_page(
|
||||
order_id,
|
||||
report_type=report_type,
|
||||
access_token=access_token,
|
||||
message=message,
|
||||
download=download,
|
||||
payment_amount=payment_amount,
|
||||
amount_selection=amount_selection,
|
||||
**kw,
|
||||
)
|
||||
18
fusion_poynt/data/poynt_surcharge_product.xml
Normal file
18
fusion_poynt/data/poynt_surcharge_product.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="product_cc_processing_fee" model="product.product">
|
||||
<field name="name">CREDIT CARD PROCESSING FEE</field>
|
||||
<field name="default_code">POYNT_CC_FEE</field>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">0.0</field>
|
||||
<field name="sale_ok" eval="False"/>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="taxes_id" eval="[(5, 0, 0)]"/>
|
||||
<field name="supplier_taxes_id" eval="[(5, 0, 0)]"/>
|
||||
<field name="description_sale">Credit card processing surcharge</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -5,4 +5,5 @@ from . import payment_provider
|
||||
from . import payment_token
|
||||
from . import payment_transaction
|
||||
from . import poynt_terminal
|
||||
from . import res_config_settings
|
||||
from . import sale_order
|
||||
|
||||
@@ -347,6 +347,21 @@ class PaymentProvider(models.Model):
|
||||
and payment_method_sudo.support_tokenization
|
||||
),
|
||||
}
|
||||
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
surcharge_enabled = ICP.get_param(
|
||||
'fusion_poynt.surcharge_enabled', 'False',
|
||||
) == 'True'
|
||||
if surcharge_enabled:
|
||||
inline_form_values['surcharge'] = {
|
||||
'enabled': True,
|
||||
'visa': float(ICP.get_param('fusion_poynt.surcharge_visa_rate', '0') or 0),
|
||||
'mastercard': float(ICP.get_param('fusion_poynt.surcharge_mastercard_rate', '0') or 0),
|
||||
'amex': float(ICP.get_param('fusion_poynt.surcharge_amex_rate', '0') or 0),
|
||||
'debit': float(ICP.get_param('fusion_poynt.surcharge_debit_rate', '0') or 0),
|
||||
'other': float(ICP.get_param('fusion_poynt.surcharge_other_rate', '0') or 0),
|
||||
}
|
||||
|
||||
return json.dumps(inline_form_values)
|
||||
|
||||
# === ACTION METHODS === #
|
||||
|
||||
@@ -92,13 +92,20 @@ class PaymentTransaction(models.Model):
|
||||
|
||||
poynt_data = self._poynt_create_order_and_authorize()
|
||||
if poynt_data:
|
||||
status = poynt_data.get('status', 'AUTHORIZED')
|
||||
payment_data = {
|
||||
'reference': self.reference,
|
||||
'poynt_order_id': poynt_data.get('order_id'),
|
||||
'poynt_transaction_id': poynt_data.get('transaction_id'),
|
||||
'poynt_status': poynt_data.get('status', 'AUTHORIZED'),
|
||||
'poynt_status': status,
|
||||
'funding_source': poynt_data.get('funding_source', {}),
|
||||
}
|
||||
if status in ('DECLINED', 'FAILED', 'REFUND_FAILED'):
|
||||
self._set_error(
|
||||
_("Payment was %(status)s by the processor.",
|
||||
status=status.lower())
|
||||
)
|
||||
return
|
||||
self._process('poynt', payment_data)
|
||||
|
||||
def _poynt_create_order_and_authorize(self):
|
||||
@@ -148,6 +155,103 @@ class PaymentTransaction(models.Model):
|
||||
self._set_error(str(e))
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def _detect_card_brand_from_details(payment_details):
|
||||
"""Detect card brand from the payment_details string on a token.
|
||||
|
||||
Tokens store details like "VISA ending in 1234" or
|
||||
"AMERICAN_EXPRESS ending in 5678".
|
||||
"""
|
||||
details = (payment_details or '').upper()
|
||||
if 'AMEX' in details or 'AMERICAN_EXPRESS' in details:
|
||||
return 'amex'
|
||||
if 'VISA' in details:
|
||||
return 'visa'
|
||||
if 'MASTER' in details:
|
||||
return 'mastercard'
|
||||
return 'other'
|
||||
|
||||
def _apply_token_surcharge(self):
|
||||
"""Apply surcharge to the linked invoice for token-based payments.
|
||||
|
||||
Checks if surcharge is enabled, detects card brand from the token,
|
||||
adds a surcharge line to the invoice, and updates the transaction
|
||||
amount. Skips rental orders (recurring charges should not get
|
||||
surcharge), invoices with no linked records, or invoices where
|
||||
surcharge is already applied.
|
||||
"""
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
if ICP.get_param('fusion_poynt.surcharge_enabled', 'False') != 'True':
|
||||
return
|
||||
|
||||
if not self.token_id or not self.invoice_ids:
|
||||
return
|
||||
|
||||
for inv in self.invoice_ids:
|
||||
sale_orders = inv.mapped('line_ids.sale_line_ids.order_id')
|
||||
for so in sale_orders:
|
||||
if getattr(so, 'is_rental_order', False):
|
||||
if not getattr(so, 'rental_apply_cc_fee', True):
|
||||
return
|
||||
|
||||
card_type = self._detect_card_brand_from_details(
|
||||
self.token_id.payment_details,
|
||||
)
|
||||
rate_key = {
|
||||
'visa': 'fusion_poynt.surcharge_visa_rate',
|
||||
'mastercard': 'fusion_poynt.surcharge_mastercard_rate',
|
||||
'amex': 'fusion_poynt.surcharge_amex_rate',
|
||||
'debit': 'fusion_poynt.surcharge_debit_rate',
|
||||
}.get(card_type, 'fusion_poynt.surcharge_other_rate')
|
||||
rate = float(ICP.get_param(rate_key, '0') or 0)
|
||||
if rate <= 0:
|
||||
return
|
||||
|
||||
product_id = int(ICP.get_param('fusion_poynt.surcharge_product_id', '0') or 0)
|
||||
product = self.env['product.product'].sudo().browse(product_id).exists()
|
||||
if not product:
|
||||
product = self.env.ref(
|
||||
'fusion_poynt.product_cc_processing_fee', raise_if_not_found=False,
|
||||
)
|
||||
if not product:
|
||||
_logger.warning("Surcharge product not configured; skipping token surcharge")
|
||||
return
|
||||
|
||||
total_fee = 0.0
|
||||
for invoice in self.invoice_ids.sudo():
|
||||
already_has = invoice.invoice_line_ids.filtered(
|
||||
lambda l: l.product_id.id == product.id
|
||||
)
|
||||
if already_has:
|
||||
continue
|
||||
|
||||
fee_amount = round(invoice.amount_residual * rate / 100.0, 2)
|
||||
if fee_amount <= 0:
|
||||
continue
|
||||
|
||||
was_posted = invoice.state == 'posted'
|
||||
if was_posted:
|
||||
invoice.button_draft()
|
||||
|
||||
description = "Credit Card Processing Fee (%.2f%% surcharge)" % rate
|
||||
invoice.write({
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'product_id': product.id,
|
||||
'name': description,
|
||||
'quantity': 1,
|
||||
'price_unit': fee_amount,
|
||||
'tax_ids': [(5, 0, 0)],
|
||||
})],
|
||||
})
|
||||
|
||||
if was_posted:
|
||||
invoice.action_post()
|
||||
|
||||
total_fee += fee_amount
|
||||
|
||||
if total_fee > 0:
|
||||
self.amount += total_fee
|
||||
|
||||
def _poynt_process_token_payment(self):
|
||||
"""Process a payment using a stored token (card on file).
|
||||
|
||||
@@ -156,6 +260,8 @@ class PaymentTransaction(models.Model):
|
||||
were created before the JWT migration.
|
||||
"""
|
||||
try:
|
||||
self._apply_token_surcharge()
|
||||
|
||||
provider = self._get_provider_sudo()
|
||||
action = 'AUTHORIZE' if provider.capture_manually else 'SALE'
|
||||
payment_jwt = self.token_id.poynt_payment_token
|
||||
@@ -213,13 +319,28 @@ class PaymentTransaction(models.Model):
|
||||
if order_id:
|
||||
self.poynt_order_id = order_id
|
||||
|
||||
status = txn_result.get('status', '')
|
||||
payment_data = {
|
||||
'reference': self.reference,
|
||||
'poynt_order_id': order_id,
|
||||
'poynt_transaction_id': transaction_id,
|
||||
'poynt_status': txn_result.get('status', ''),
|
||||
'poynt_status': status,
|
||||
'funding_source': txn_result.get('fundingSource', {}),
|
||||
}
|
||||
|
||||
if status in ('DECLINED', 'FAILED', 'REFUND_FAILED'):
|
||||
processor = txn_result.get('processorResponse', {})
|
||||
decline_msg = (
|
||||
processor.get('statusMessage')
|
||||
or processor.get('message')
|
||||
or status.lower()
|
||||
)
|
||||
self._set_error(
|
||||
_("Payment %(status)s: %(reason)s",
|
||||
status=status.lower(), reason=decline_msg)
|
||||
)
|
||||
return
|
||||
|
||||
self._process('poynt', payment_data)
|
||||
except ValidationError as e:
|
||||
self._set_error(str(e))
|
||||
|
||||
91
fusion_poynt/models/res_config_settings.py
Normal file
91
fusion_poynt/models/res_config_settings.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
poynt_surcharge_enabled = fields.Boolean(
|
||||
string="Enable Credit Card Surcharge",
|
||||
config_parameter='fusion_poynt.surcharge_enabled',
|
||||
)
|
||||
poynt_surcharge_visa_rate = fields.Float(
|
||||
string="Visa Rate (%)",
|
||||
config_parameter='fusion_poynt.surcharge_visa_rate',
|
||||
default=2.5,
|
||||
)
|
||||
poynt_surcharge_mastercard_rate = fields.Float(
|
||||
string="Mastercard Rate (%)",
|
||||
config_parameter='fusion_poynt.surcharge_mastercard_rate',
|
||||
default=2.5,
|
||||
)
|
||||
poynt_surcharge_amex_rate = fields.Float(
|
||||
string="Amex Rate (%)",
|
||||
config_parameter='fusion_poynt.surcharge_amex_rate',
|
||||
default=3.5,
|
||||
)
|
||||
poynt_surcharge_debit_rate = fields.Float(
|
||||
string="Debit Rate (%)",
|
||||
config_parameter='fusion_poynt.surcharge_debit_rate',
|
||||
default=0.0,
|
||||
)
|
||||
poynt_surcharge_other_rate = fields.Float(
|
||||
string="Other Cards Rate (%)",
|
||||
config_parameter='fusion_poynt.surcharge_other_rate',
|
||||
default=2.5,
|
||||
)
|
||||
poynt_surcharge_product_id = fields.Many2one(
|
||||
'product.product',
|
||||
string="Surcharge Product",
|
||||
config_parameter='fusion_poynt.surcharge_product_id',
|
||||
help="The service product used for the credit card processing fee line.",
|
||||
)
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
res = super().get_values()
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
product_id = int(ICP.get_param('fusion_poynt.surcharge_product_id', '0') or 0)
|
||||
if product_id and self.env['product.product'].sudo().browse(product_id).exists():
|
||||
res['poynt_surcharge_product_id'] = product_id
|
||||
else:
|
||||
default = self.env.ref('fusion_poynt.product_cc_processing_fee', raise_if_not_found=False)
|
||||
res['poynt_surcharge_product_id'] = default.id if default else False
|
||||
return res
|
||||
|
||||
def set_values(self):
|
||||
super().set_values()
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
ICP.set_param(
|
||||
'fusion_poynt.surcharge_product_id',
|
||||
str(self.poynt_surcharge_product_id.id) if self.poynt_surcharge_product_id else '0',
|
||||
)
|
||||
|
||||
def action_open_poynt_provider(self):
|
||||
provider = self.env['payment.provider'].sudo().search(
|
||||
[('code', '=', 'poynt')], limit=1,
|
||||
)
|
||||
if provider:
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'payment.provider',
|
||||
'res_id': provider.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'payment.provider',
|
||||
'view_mode': 'list,form',
|
||||
'target': 'current',
|
||||
'domain': [('code', '=', 'poynt')],
|
||||
}
|
||||
|
||||
def action_open_poynt_terminals(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'poynt.terminal',
|
||||
'view_mode': 'list,form',
|
||||
'target': 'current',
|
||||
}
|
||||
@@ -11,6 +11,8 @@ patch(PaymentForm.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.poyntFormData = {};
|
||||
this._detectedCardType = 'other';
|
||||
this._selectedCardType = 'other';
|
||||
},
|
||||
|
||||
// #=== DOM MANIPULATION ===#
|
||||
@@ -42,6 +44,77 @@ patch(PaymentForm.prototype, {
|
||||
|
||||
this._setupCardFormatting(poyntContainer);
|
||||
this._setupTerminalToggle(poyntContainer);
|
||||
this._setupSurcharge(poyntContainer);
|
||||
},
|
||||
|
||||
_detectCardBrand(number) {
|
||||
const num = (number || '').replace(/\D/g, '');
|
||||
if (num.length < 2) return 'other';
|
||||
const prefix2 = num.substring(0, 2);
|
||||
if (prefix2 === '34' || prefix2 === '37') return 'amex';
|
||||
if (num[0] === '4') return 'visa';
|
||||
const p2 = parseInt(prefix2, 10);
|
||||
if (p2 >= 51 && p2 <= 55) return 'mastercard';
|
||||
if (num.length >= 4) {
|
||||
const p4 = parseInt(num.substring(0, 4), 10);
|
||||
if (p4 >= 2221 && p4 <= 2720) return 'mastercard';
|
||||
}
|
||||
return 'other';
|
||||
},
|
||||
|
||||
_setupSurcharge(container) {
|
||||
const surchargeConfig = this.poyntFormData.surcharge;
|
||||
if (!surchargeConfig || !surchargeConfig.enabled) return;
|
||||
|
||||
const cardTypeSection = container.querySelector('.o_poynt_card_type_section');
|
||||
const surchargeNotice = container.querySelector('.o_poynt_surcharge_notice');
|
||||
|
||||
if (cardTypeSection) {
|
||||
cardTypeSection.style.display = 'block';
|
||||
}
|
||||
|
||||
const cardTypeRadios = container.querySelectorAll('input[name="poynt_card_type"]');
|
||||
cardTypeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
this._selectedCardType = radio.value;
|
||||
this._updateSurchargeDisplay(container);
|
||||
});
|
||||
});
|
||||
|
||||
this._updateSurchargeDisplay(container);
|
||||
},
|
||||
|
||||
_updateSurchargeDisplay(container) {
|
||||
const surchargeConfig = this.poyntFormData.surcharge;
|
||||
if (!surchargeConfig || !surchargeConfig.enabled) return;
|
||||
|
||||
const cardType = this._detectedCardType !== 'other'
|
||||
? this._detectedCardType
|
||||
: this._selectedCardType;
|
||||
|
||||
const rate = surchargeConfig[cardType] || surchargeConfig['other'] || 0;
|
||||
const amount = this.poyntFormData.minor_amount || 0;
|
||||
const currencyName = this.poyntFormData.currency_name || 'CAD';
|
||||
|
||||
const baseAmount = amount / 100;
|
||||
const feeAmount = Math.round(baseAmount * rate) / 100;
|
||||
|
||||
const rateEl = container.querySelector('#poynt_surcharge_rate');
|
||||
const amountEl = container.querySelector('#poynt_surcharge_amount');
|
||||
const noticeEl = container.querySelector('.o_poynt_surcharge_notice');
|
||||
|
||||
if (rateEl) rateEl.textContent = rate.toFixed(2);
|
||||
if (amountEl) amountEl.textContent = `$${feeAmount.toFixed(2)}`;
|
||||
if (noticeEl) {
|
||||
noticeEl.style.display = rate > 0 ? 'block' : 'none';
|
||||
}
|
||||
|
||||
const radioToCheck = container.querySelector(
|
||||
`input[name="poynt_card_type"][value="${cardType}"]`
|
||||
);
|
||||
if (radioToCheck && !radioToCheck.checked) {
|
||||
radioToCheck.checked = true;
|
||||
}
|
||||
},
|
||||
|
||||
_setupCardFormatting(container) {
|
||||
@@ -57,6 +130,17 @@ patch(PaymentForm.prototype, {
|
||||
formatted += value[i];
|
||||
}
|
||||
e.target.value = formatted;
|
||||
|
||||
const detected = this._detectCardBrand(value);
|
||||
if (detected !== this._detectedCardType) {
|
||||
this._detectedCardType = detected;
|
||||
if (detected !== 'other') {
|
||||
this._selectedCardType = detected;
|
||||
}
|
||||
this._updateSurchargeDisplay(
|
||||
e.target.closest('.o_poynt_payment_form')
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -228,11 +312,19 @@ patch(PaymentForm.prototype, {
|
||||
}
|
||||
},
|
||||
|
||||
_getSelectedCardType(inlineForm) {
|
||||
const checked = inlineForm.querySelector('input[name="poynt_card_type"]:checked');
|
||||
return checked ? checked.value : 'other';
|
||||
},
|
||||
|
||||
async _processCardPayment(processingValues, inlineForm) {
|
||||
const cardNumber = inlineForm.querySelector('#poynt_card_number').value.replace(/\D/g, '');
|
||||
const expiry = inlineForm.querySelector('#poynt_expiry').value;
|
||||
const cvv = inlineForm.querySelector('#poynt_cvv').value;
|
||||
const cardholder = inlineForm.querySelector('#poynt_cardholder').value;
|
||||
const cardType = this._detectedCardType !== 'other'
|
||||
? this._detectedCardType
|
||||
: this._getSelectedCardType(inlineForm);
|
||||
|
||||
const [expMonth, expYear] = expiry.split('/').map(Number);
|
||||
|
||||
@@ -245,6 +337,7 @@ patch(PaymentForm.prototype, {
|
||||
exp_year: 2000 + expYear,
|
||||
cvv: cvv,
|
||||
cardholder_name: cardholder,
|
||||
card_type: cardType,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
@@ -268,12 +361,14 @@ patch(PaymentForm.prototype, {
|
||||
|
||||
async _processTerminalPayment(processingValues, inlineForm) {
|
||||
const terminalId = inlineForm.querySelector('#poynt_terminal_select').value;
|
||||
const cardType = this._getSelectedCardType(inlineForm);
|
||||
|
||||
try {
|
||||
const result = await rpc('/payment/poynt/send_to_terminal', {
|
||||
reference: processingValues.reference,
|
||||
terminal_id: parseInt(terminalId),
|
||||
poynt_order_id: processingValues.poynt_order_id,
|
||||
card_type: cardType,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
|
||||
@@ -62,6 +62,45 @@
|
||||
autocomplete="cc-name"/>
|
||||
</div>
|
||||
|
||||
<!-- Card type selector (for terminal payments where card brand cannot be auto-detected) -->
|
||||
<div class="mb-3 o_poynt_card_type_section" style="display:none;">
|
||||
<label class="form-label">Card Type</label>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<div class="form-check">
|
||||
<input type="radio" class="form-check-input" name="poynt_card_type"
|
||||
id="poynt_ct_visa" value="visa"/>
|
||||
<label class="form-check-label" for="poynt_ct_visa">Visa</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" class="form-check-input" name="poynt_card_type"
|
||||
id="poynt_ct_mc" value="mastercard"/>
|
||||
<label class="form-check-label" for="poynt_ct_mc">Mastercard</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" class="form-check-input" name="poynt_card_type"
|
||||
id="poynt_ct_amex" value="amex"/>
|
||||
<label class="form-check-label" for="poynt_ct_amex">Amex</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" class="form-check-input" name="poynt_card_type"
|
||||
id="poynt_ct_other" value="other" checked="checked"/>
|
||||
<label class="form-check-label" for="poynt_ct_other">Other</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Surcharge notice (shown when surcharge is enabled) -->
|
||||
<div class="mb-3 o_poynt_surcharge_notice" style="display:none;">
|
||||
<div class="alert alert-info py-2 mb-0">
|
||||
<small>
|
||||
<i class="fa fa-info-circle me-1"/>
|
||||
<span>A credit card processing fee of </span>
|
||||
<strong id="poynt_surcharge_rate">0.00</strong>
|
||||
<span>% (<strong id="poynt_surcharge_amount">$0.00</strong>) will be added to your total.</span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terminal payment option -->
|
||||
<div class="mb-3 o_poynt_terminal_section" style="display:none;">
|
||||
<hr/>
|
||||
|
||||
146
fusion_poynt/views/res_config_settings_views.xml
Normal file
146
fusion_poynt/views/res_config_settings_views.xml
Normal file
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="res_config_settings_view_form_fusion_poynt" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.fusion.poynt</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form" position="inside">
|
||||
<app data-string="Fusion Poynt" string="Fusion Poynt" name="fusion_poynt"
|
||||
groups="fusion_poynt.group_fusion_poynt_admin">
|
||||
|
||||
<h2>Credit Card Surcharge</h2>
|
||||
<div class="row mt-4 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="poynt_surcharge_enabled"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Credit Card Processing Fee</span>
|
||||
<div class="text-muted">
|
||||
Automatically add a surcharge line to invoices when collecting payment
|
||||
via Poynt. The fee is calculated as a percentage of the invoice total.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4 o_settings_container"
|
||||
invisible="not poynt_surcharge_enabled">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Surcharge Rates by Card Type</span>
|
||||
<div class="text-muted mb-2">
|
||||
Configure the processing fee percentage for each card brand.
|
||||
The surcharge is added as a separate invoice line before payment.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<div class="row mb-2">
|
||||
<label for="poynt_surcharge_visa_rate"
|
||||
class="col-5 col-form-label">Visa</label>
|
||||
<div class="col-4">
|
||||
<field name="poynt_surcharge_visa_rate" class="o_input"/>
|
||||
</div>
|
||||
<div class="col-1 col-form-label">%</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<label for="poynt_surcharge_mastercard_rate"
|
||||
class="col-5 col-form-label">Mastercard</label>
|
||||
<div class="col-4">
|
||||
<field name="poynt_surcharge_mastercard_rate" class="o_input"/>
|
||||
</div>
|
||||
<div class="col-1 col-form-label">%</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<label for="poynt_surcharge_amex_rate"
|
||||
class="col-5 col-form-label">American Express</label>
|
||||
<div class="col-4">
|
||||
<field name="poynt_surcharge_amex_rate" class="o_input"/>
|
||||
</div>
|
||||
<div class="col-1 col-form-label">%</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<label for="poynt_surcharge_debit_rate"
|
||||
class="col-5 col-form-label">Debit</label>
|
||||
<div class="col-4">
|
||||
<field name="poynt_surcharge_debit_rate" class="o_input"/>
|
||||
</div>
|
||||
<div class="col-1 col-form-label">%</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<label for="poynt_surcharge_other_rate"
|
||||
class="col-5 col-form-label">Other Cards</label>
|
||||
<div class="col-4">
|
||||
<field name="poynt_surcharge_other_rate" class="o_input"/>
|
||||
</div>
|
||||
<div class="col-1 col-form-label">%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Surcharge Product</span>
|
||||
<div class="text-muted mb-2">
|
||||
The service product used for the processing fee invoice line.
|
||||
Must be a service product with no taxes applied.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="poynt_surcharge_product_id"
|
||||
domain="[('type', '=', 'service')]"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Quick Links</h2>
|
||||
<div class="row mt-4 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Payment Provider</span>
|
||||
<div class="text-muted mb-2">
|
||||
Configure your Poynt API credentials, business ID,
|
||||
and default terminal.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button name="action_open_poynt_provider"
|
||||
type="object"
|
||||
string="Configure Payment Provider"
|
||||
class="btn-link"
|
||||
icon="fa-arrow-right"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Terminals</span>
|
||||
<div class="text-muted mb-2">
|
||||
View and manage your Poynt terminal devices.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button name="action_open_poynt_terminals"
|
||||
type="object"
|
||||
string="Manage Terminals"
|
||||
class="btn-link"
|
||||
icon="fa-arrow-right"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</app>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fusion_poynt_settings" model="ir.actions.act_window">
|
||||
<field name="name">Fusion Poynt Settings</field>
|
||||
<field name="res_model">res.config.settings</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">current</field>
|
||||
<field name="context" eval="{'module': 'fusion_poynt'}"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -58,6 +58,37 @@ class PoyntPaymentWizard(models.TransientModel):
|
||||
default='terminal',
|
||||
)
|
||||
|
||||
# --- Card type & surcharge fields ---
|
||||
card_type = fields.Selection(
|
||||
selection=[
|
||||
('visa', "Visa"),
|
||||
('mastercard', "Mastercard"),
|
||||
('amex', "American Express"),
|
||||
('debit', "Debit"),
|
||||
('other', "Other"),
|
||||
],
|
||||
string="Card Type",
|
||||
)
|
||||
surcharge_enabled = fields.Boolean(
|
||||
compute='_compute_surcharge_enabled',
|
||||
)
|
||||
surcharge_rate = fields.Float(
|
||||
string="Surcharge Rate (%)",
|
||||
digits=(5, 2),
|
||||
readonly=True,
|
||||
)
|
||||
surcharge_amount = fields.Monetary(
|
||||
string="Surcharge Amount",
|
||||
currency_field='currency_id',
|
||||
readonly=True,
|
||||
)
|
||||
surcharge_applied = fields.Boolean(default=False)
|
||||
original_amount = fields.Monetary(
|
||||
string="Invoice Amount",
|
||||
currency_field='currency_id',
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
# --- Terminal fields ---
|
||||
terminal_id = fields.Many2one(
|
||||
'poynt.terminal',
|
||||
@@ -102,6 +133,58 @@ class PoyntPaymentWizard(models.TransientModel):
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
@api.depends_context('uid')
|
||||
def _compute_surcharge_enabled(self):
|
||||
enabled = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'fusion_poynt.surcharge_enabled', 'False',
|
||||
) == 'True'
|
||||
for rec in self:
|
||||
rec.surcharge_enabled = enabled
|
||||
|
||||
@staticmethod
|
||||
def _detect_card_brand(card_number):
|
||||
num = (card_number or '').replace(' ', '')
|
||||
if len(num) < 2:
|
||||
return 'other'
|
||||
if num[:2] in ('34', '37'):
|
||||
return 'amex'
|
||||
if num[0] == '4':
|
||||
return 'visa'
|
||||
prefix2 = int(num[:2])
|
||||
if 51 <= prefix2 <= 55:
|
||||
return 'mastercard'
|
||||
if len(num) >= 4:
|
||||
prefix4 = int(num[:4])
|
||||
if 2221 <= prefix4 <= 2720:
|
||||
return 'mastercard'
|
||||
return 'other'
|
||||
|
||||
def _get_surcharge_rate(self, card_type):
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
rate_key = {
|
||||
'visa': 'fusion_poynt.surcharge_visa_rate',
|
||||
'mastercard': 'fusion_poynt.surcharge_mastercard_rate',
|
||||
'amex': 'fusion_poynt.surcharge_amex_rate',
|
||||
'debit': 'fusion_poynt.surcharge_debit_rate',
|
||||
}.get(card_type, 'fusion_poynt.surcharge_other_rate')
|
||||
return float(ICP.get_param(rate_key, '0') or 0)
|
||||
|
||||
@api.onchange('card_number')
|
||||
def _onchange_card_number(self):
|
||||
if self.payment_mode == 'card' and self.card_number:
|
||||
self.card_type = self._detect_card_brand(self.card_number)
|
||||
|
||||
@api.onchange('card_type')
|
||||
def _onchange_card_type(self):
|
||||
if not self.card_type or not self.surcharge_enabled:
|
||||
self.surcharge_rate = 0.0
|
||||
self.surcharge_amount = 0.0
|
||||
return
|
||||
rate = self._get_surcharge_rate(self.card_type)
|
||||
base_amount = self.original_amount or self.amount
|
||||
self.surcharge_rate = rate
|
||||
self.surcharge_amount = round(base_amount * rate / 100.0, 2)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
@@ -112,6 +195,7 @@ class PoyntPaymentWizard(models.TransientModel):
|
||||
invoice = self.env['account.move'].browse(invoice_id)
|
||||
res['invoice_id'] = invoice.id
|
||||
res['amount'] = invoice.amount_residual
|
||||
res['original_amount'] = invoice.amount_residual
|
||||
res['currency_id'] = invoice.currency_id.id
|
||||
|
||||
provider = self.env['payment.provider'].sudo().search([
|
||||
@@ -135,10 +219,103 @@ class PoyntPaymentWizard(models.TransientModel):
|
||||
if provider.poynt_default_terminal_id:
|
||||
self.terminal_id = provider.poynt_default_terminal_id
|
||||
|
||||
def _apply_surcharge_if_needed(self):
|
||||
"""Add the surcharge invoice line if surcharge is enabled and not yet applied."""
|
||||
if self.surcharge_applied or not self.surcharge_enabled:
|
||||
return
|
||||
if not self.card_type:
|
||||
raise UserError(_("Please select the card type to calculate the surcharge."))
|
||||
|
||||
rate = self._get_surcharge_rate(self.card_type)
|
||||
if rate <= 0:
|
||||
return
|
||||
|
||||
base_amount = self.original_amount or self.amount
|
||||
fee_amount = round(base_amount * rate / 100.0, 2)
|
||||
if fee_amount <= 0:
|
||||
return
|
||||
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
product_id = int(ICP.get_param('fusion_poynt.surcharge_product_id', '0') or 0)
|
||||
product = self.env['product.product'].sudo().browse(product_id).exists()
|
||||
if not product:
|
||||
product = self.env.ref('fusion_poynt.product_cc_processing_fee', raise_if_not_found=False)
|
||||
if not product:
|
||||
raise UserError(
|
||||
_("Surcharge product not configured. "
|
||||
"Go to Settings > Fusion Poynt to set it up.")
|
||||
)
|
||||
|
||||
invoice = self.invoice_id.sudo()
|
||||
|
||||
was_posted = invoice.state == 'posted'
|
||||
if was_posted:
|
||||
invoice.button_draft()
|
||||
|
||||
description = _("Credit Card Processing Fee (%(rate).2f%% surcharge)", rate=rate)
|
||||
invoice.write({
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'product_id': product.id,
|
||||
'name': description,
|
||||
'quantity': 1,
|
||||
'price_unit': fee_amount,
|
||||
'tax_ids': [(5, 0, 0)],
|
||||
})],
|
||||
})
|
||||
|
||||
if was_posted:
|
||||
invoice.action_post()
|
||||
|
||||
self.write({
|
||||
'surcharge_applied': True,
|
||||
'surcharge_rate': rate,
|
||||
'surcharge_amount': fee_amount,
|
||||
'amount': invoice.amount_residual,
|
||||
})
|
||||
|
||||
def _remove_surcharge_line(self):
|
||||
"""Remove the surcharge line from the invoice if it was applied."""
|
||||
if not self.surcharge_applied:
|
||||
return
|
||||
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
product_id = int(ICP.get_param('fusion_poynt.surcharge_product_id', '0') or 0)
|
||||
product = self.env['product.product'].sudo().browse(product_id).exists()
|
||||
if not product:
|
||||
product = self.env.ref('fusion_poynt.product_cc_processing_fee', raise_if_not_found=False)
|
||||
if not product:
|
||||
return
|
||||
|
||||
invoice = self.invoice_id.sudo()
|
||||
surcharge_lines = invoice.invoice_line_ids.filtered(
|
||||
lambda l: l.product_id.id == product.id
|
||||
)
|
||||
if not surcharge_lines:
|
||||
self.surcharge_applied = False
|
||||
return
|
||||
|
||||
was_posted = invoice.state == 'posted'
|
||||
if was_posted:
|
||||
invoice.button_draft()
|
||||
|
||||
surcharge_lines.unlink()
|
||||
|
||||
if was_posted:
|
||||
invoice.action_post()
|
||||
|
||||
self.write({
|
||||
'surcharge_applied': False,
|
||||
'surcharge_amount': 0.0,
|
||||
'surcharge_rate': 0.0,
|
||||
'amount': invoice.amount_residual,
|
||||
})
|
||||
|
||||
def action_collect_payment(self):
|
||||
"""Dispatch to the appropriate payment method."""
|
||||
self.ensure_one()
|
||||
|
||||
self._apply_surcharge_if_needed()
|
||||
|
||||
if self.amount <= 0:
|
||||
raise UserError(_("Payment amount must be greater than zero."))
|
||||
|
||||
@@ -187,6 +364,7 @@ class PoyntPaymentWizard(models.TransientModel):
|
||||
|
||||
except (ValidationError, UserError) as e:
|
||||
self._cleanup_draft_transaction()
|
||||
self._remove_surcharge_line()
|
||||
self.write({
|
||||
'state': 'error',
|
||||
'status_message': str(e),
|
||||
@@ -273,6 +451,31 @@ class PoyntPaymentWizard(models.TransientModel):
|
||||
'poynt_status': status,
|
||||
'funding_source': result.get('fundingSource', {}),
|
||||
}
|
||||
|
||||
if status in ('DECLINED', 'FAILED', 'REFUND_FAILED'):
|
||||
tx._set_error(
|
||||
_("Payment was %(status)s by the processor.",
|
||||
status=status.lower())
|
||||
)
|
||||
self._cleanup_draft_transaction()
|
||||
self._remove_surcharge_line()
|
||||
processor = result.get('processorResponse', {})
|
||||
decline_msg = (
|
||||
processor.get('statusMessage')
|
||||
or processor.get('message')
|
||||
or status.lower()
|
||||
)
|
||||
self.write({
|
||||
'state': 'error',
|
||||
'status_message': _(
|
||||
"Payment %(status)s: %(reason)s",
|
||||
status=status.lower(),
|
||||
reason=decline_msg,
|
||||
),
|
||||
'poynt_transaction_ref': transaction_id,
|
||||
})
|
||||
return self._reopen_wizard()
|
||||
|
||||
tx._process('poynt', payment_data)
|
||||
|
||||
self.write({
|
||||
@@ -288,6 +491,7 @@ class PoyntPaymentWizard(models.TransientModel):
|
||||
|
||||
except (ValidationError, UserError) as e:
|
||||
self._cleanup_draft_transaction()
|
||||
self._remove_surcharge_line()
|
||||
self.write({
|
||||
'state': 'error',
|
||||
'status_message': str(e),
|
||||
@@ -352,6 +556,7 @@ class PoyntPaymentWizard(models.TransientModel):
|
||||
|
||||
if status in ('DECLINED', 'VOIDED', 'REFUNDED'):
|
||||
self._cleanup_draft_transaction()
|
||||
self._remove_surcharge_line()
|
||||
self.write({
|
||||
'state': 'error',
|
||||
'status_message': _(
|
||||
@@ -473,6 +678,7 @@ class PoyntPaymentWizard(models.TransientModel):
|
||||
"""Cancel the payment and clean up the draft transaction."""
|
||||
self.ensure_one()
|
||||
self._cleanup_draft_transaction()
|
||||
self._remove_surcharge_line()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def _cleanup_draft_transaction(self):
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="poynt_transaction_ref" invisible="1"/>
|
||||
<field name="provider_id" invisible="1"/>
|
||||
<field name="surcharge_enabled" invisible="1"/>
|
||||
<field name="surcharge_applied" invisible="1"/>
|
||||
<field name="original_amount" invisible="1"/>
|
||||
|
||||
<!-- Status banner for waiting / done / error -->
|
||||
<div class="alert alert-info" role="alert"
|
||||
@@ -44,6 +47,25 @@
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Card Type & Surcharge section -->
|
||||
<group string="Card Type & Surcharge"
|
||||
invisible="state == 'done' or not surcharge_enabled">
|
||||
<group>
|
||||
<field name="card_type"
|
||||
widget="radio"
|
||||
required="surcharge_enabled and state in ('draft', 'error')"
|
||||
readonly="state not in ('draft', 'error')"/>
|
||||
</group>
|
||||
<group invisible="not card_type">
|
||||
<field name="surcharge_rate" string="Rate (%)"/>
|
||||
<field name="surcharge_amount"/>
|
||||
<div class="text-muted" colspan="2"
|
||||
invisible="surcharge_amount == 0">
|
||||
A surcharge line will be added to the invoice before payment.
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Terminal section -->
|
||||
<group string="Terminal"
|
||||
invisible="payment_mode != 'terminal' or state == 'done'">
|
||||
|
||||
Reference in New Issue
Block a user