Skip to content

Commit

Permalink
[IMP] website_sale_sepa_dd_payment: payment provider for sepa dd
Browse files Browse the repository at this point in the history
  • Loading branch information
remytms committed Nov 27, 2024
1 parent 6d6f199 commit bd6635f
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 150 deletions.
7 changes: 7 additions & 0 deletions website_sale_sepa_dd_payment/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,19 @@
"application": False,
"depends": [
"website_sale",
"payment_custom",
],
"excludes": [],
"data": [
"views/product_views.xml",
"views/templates.xml",
"data/payment_provider_data.xml",
],
"assets": {
"web.assets_frontend": [
"website_sale_sepa_dd_payment/static/src/js/payment_form.js",
],
},
"demo": [],
"qweb": [],
}
50 changes: 24 additions & 26 deletions website_sale_sepa_dd_payment/controllers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,34 @@
#
# SPDX-License-Identifier: AGPL-3.0-or-later

from odoo import http
from odoo import _
from odoo.exceptions import ValidationError
from odoo.http import request

from odoo.addons.website_sale.controllers.main import WebsiteSale
from odoo.addons.website_sale.controllers.main import PaymentPortal


class WebsiteSaleSEPADirectDebit(WebsiteSale):
@http.route(
"/shop/payment", type="http", auth="public", website=True, sitemap=False
)
def shop_payment(self, **post):
sepa_dd_errors = []
sepa_dd_iban = ""
# Set default values
if request.env.user.partner_id.bank_ids:
sepa_dd_iban = request.env.user.partner_id.bank_ids[0].acc_number
# Process form
if request.httprequest.method == "POST":
order = request.website.sale_get_order()
order.sepa_dd_iban = request.params.get("sepa-dd-iban", "")
class PaymentPortalSEPADD(PaymentPortal):
def _validate_transaction_for_order(self, transaction, sale_order_id):
"""Throws a ValidationError if IBAN is not correct"""
if (
transaction.provider_id.code == "custom"
and transaction.provider_id.custom_mode == "sepa_dd"
):
sepa_dd_accept = request.params.get("sepa_dd_accept")
if not sepa_dd_accept:
raise ValidationError(
_("You must accept to give a SEPA Direct Debit Mandate.")
)
sepa_dd_iban = request.params.get("sepa_dd_iban", "")
if not sepa_dd_iban:
raise ValidationError(
_("IBAN must be provided for SEPA Direct Debit payment")
)
transaction.sepa_dd_iban = sepa_dd_iban

# Set SEPA Direct Debit payment_mode_id
order = request.env["sale.order"].sudo().browse(sale_order_id).exists()
order.payment_mode_id = order.get_sepa_dd_payment_mode()
try:
order.with_context(send_email=True).action_confirm()
except ValidationError as err:
sepa_dd_errors.append(str(err))
else:
return request.redirect(order.get_portal_url())

result = super().shop_payment(**post)
result.qcontext["sepa_dd_errors"] = sepa_dd_errors
result.qcontext["sepa_dd_iban"] = sepa_dd_iban
return result
return super()._validate_transaction_for_order(transaction, sale_order_id)
19 changes: 19 additions & 0 deletions website_sale_sepa_dd_payment/data/payment_provider_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">

<record id="payment_provider_sepa_dd" model="payment.provider">
<field name="name">SEPA Direct Debit</field>
<field name="module_id" ref="base.module_website_sale_sepa_dd_payment" />
<field name="code">custom</field>
<field name="custom_mode">sepa_dd</field>
<field name="state">enabled</field>
<field name="inline_form_view_id" ref="inline_form" />
<field name="pending_msg" type="html">
<p>
<i>Your order has been saved.</i>
Your payment by SEPA Dierct Debit will be processed soon.
</p>
</field>
</record>

</odoo>
2 changes: 2 additions & 0 deletions website_sale_sepa_dd_payment/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
from . import sale_order
from . import product_template
from . import payment_provider
from . import payment_transaction
37 changes: 37 additions & 0 deletions website_sale_sepa_dd_payment/models/payment_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# SPDX-FileCopyrightText: 2024 Coop IT Easy SC
#
# SPDX-License-Identifier: AGPL-3.0-or-later

from odoo import api, fields, models


class PaymentProvider(models.Model):
_inherit = "payment.provider"

custom_mode = fields.Selection(selection_add=[("sepa_dd", "SEPA Direct Debit")])

@api.model
def _get_compatible_providers(
self, *args, sale_order_id=None, website_id=None, **kwargs
):
"""Override of payment to exclude onsite providers if the delivery doesn't match.
:param int sale_order_id: The sale order to be paid, if any, as a `sale.order` id
:param int website_id: The provided website, as a `website` id
:return: The compatible providers
:rtype: recordset of `payment.provider`
"""
compatible_providers = super()._get_compatible_providers(
*args, sale_order_id=sale_order_id, website_id=website_id, **kwargs
)
order = self.env["sale.order"].browse(sale_order_id).exists()
# Allow SEPA Direct Debit only if all products allow SEPA DD payment
allow_sepa_dd_payment = all(
order.order_line.mapped("product_id").mapped("allow_sepa_dd_payment")
)
if not allow_sepa_dd_payment:
compatible_providers = compatible_providers.filtered(
lambda p: p.code != "custom" or p.custom_mode != "sepa_dd"
)

return compatible_providers
82 changes: 82 additions & 0 deletions website_sale_sepa_dd_payment/models/payment_transaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# SPDX-FileCopyrightText: 2024 Coop IT Easy SC
#
# SPDX-License-Identifier: AGPL-3.0-or-later

import datetime

from odoo import api, fields, models
from odoo.exceptions import ValidationError

from odoo.addons.base.models.res_bank import sanitize_account_number
from odoo.addons.base_iban.models.res_partner_bank import validate_iban


class PaymentTransaction(models.Model):
_inherit = "payment.transaction"

sepa_dd_iban = fields.Char(string="IBAN (for SEPA Direct Debit)")

@api.constrains("sepa_dd_iban", "partner_id")
def _check_sepa_dd_iban(self):
"""Raise ValidationError if sepa_dd_iban is not correct"""
if self.sepa_dd_iban:
validate_iban(self.sepa_dd_iban)
sanitized_sepa_dd_iban = sanitize_account_number(self.sepa_dd_iban)
res_partner_bank = self.env["res.partner.bank"].search(
[("sanitized_acc_number", "=", sanitized_sepa_dd_iban)],
limit=1,
)
if res_partner_bank and res_partner_bank.partner_id != self.partner_id:
raise ValidationError(
f"The account number {sanitized_sepa_dd_iban} does not belongs "
f"to {self.partner_id.name}."
)
return res_partner_bank

def _set_pending(self, state_message=None):
txs_to_process = super()._set_pending(state_message=state_message)
txs_to_process.create_sepa_dd_mandate()
return txs_to_process

def get_res_partner_bank(self):
"""Return res.partner.bank that match the sepa_dd_iban
account.
"""
self.ensure_one()
res_partner_bank = self.env["res.partner.bank"]
if self.sepa_dd_iban:
sanitized_sepa_dd_iban = sanitize_account_number(self.sepa_dd_iban)
res_partner_bank = self.env["res.partner.bank"].search(
[("sanitized_acc_number", "=", sanitized_sepa_dd_iban)],
limit=1,
)
return res_partner_bank

def sepa_dd_mandate_vals(self):
self.ensure_one()
return {
"format": "sepa",
"type": "recurrent",
"scheme": "CORE",
"recurrent_sequence_type": "recurring",
"signature_date": datetime.datetime.now(),
"state": "valid",
}

def create_sepa_dd_mandate(self):
for tx in self:
res_partner_bank = tx.get_res_partner_bank()
if not res_partner_bank:
res_partner_bank = self.env["res.partner.bank"].create(
{
"partner_id": tx.partner_id.id,
"acc_number": tx.sepa_dd_iban,
}
)
res_partner_bank.write(
{
"mandate_ids": [
(0, 0, tx.sepa_dd_mandate_vals()),
],
}
)
58 changes: 0 additions & 58 deletions website_sale_sepa_dd_payment/models/sale_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,14 @@
#
# SPDX-License-Identifier: AGPL-3.0-or-later

import datetime

from odoo import api, fields, models
from odoo.exceptions import ValidationError

from odoo.addons.base.models.res_bank import sanitize_account_number
from odoo.addons.base_iban.models.res_partner_bank import validate_iban


class SaleOrder(models.Model):
_inherit = "sale.order"

allow_sepa_dd_payment = fields.Boolean(compute="_compute_allow_sepa_dd_payment")
sepa_dd_iban = fields.Char()

@api.depends("order_line", "payment_mode_id")
def _compute_allow_sepa_dd_payment(self):
Expand All @@ -30,55 +24,3 @@ def get_sepa_dd_payment_mode(self):
[("payment_method_id.code", "=", "sepa_direct_debit")],
limit=1,
)

def action_confirm(self):
sepa_dd_payment_mode = self.get_sepa_dd_payment_mode()
for order in self:
if order.payment_mode_id == sepa_dd_payment_mode:
order.create_sepa_dd_mandate()
return super().action_confirm()

def validate_sepa_dd_iban(self):
"""Raise ValidationError if sepa_dd_iban is not correct"""
self.ensure_one()
validate_iban(self.sepa_dd_iban)
sanitized_sepa_dd_iban = sanitize_account_number(self.sepa_dd_iban)
res_partner_bank = self.env["res.partner.bank"].search(
[("sanitized_acc_number", "=", sanitized_sepa_dd_iban)],
limit=1,
)
if res_partner_bank and res_partner_bank.partner_id != self.partner_id:
raise ValidationError(
f"The account number {sanitized_sepa_dd_iban} does not belongs "
f"to {self.partner_id.name}."
)
return res_partner_bank

def sepa_dd_mandate_vals(self):
self.ensure_one()
return {
"format": "sepa",
"type": "recurrent",
"scheme": "CORE",
"recurrent_sequence_type": "recurring",
"signature_date": datetime.datetime.now(),
"state": "valid",
}

def create_sepa_dd_mandate(self):
for order in self:
res_partner_bank = order.validate_sepa_dd_iban()
if not res_partner_bank:
res_partner_bank = self.env["res.partner.bank"].create(
{
"partner_id": order.partner_id.id,
"acc_number": order.sepa_dd_iban,
}
)
res_partner_bank.write(
{
"mandate_ids": [
(0, 0, order.sepa_dd_mandate_vals()),
],
}
)
83 changes: 83 additions & 0 deletions website_sale_sepa_dd_payment/static/src/js/payment_form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
odoo.define("website_sale_sepa_dd_payment.payment_form", (require) => {
"use strict";

const checkoutForm = require("payment.checkout_form");
const manageForm = require("payment.manage_form");

const sepaDDMixin = {
/**
* Return all relevant inline form inputs
*
* @private
* @param {Number} providerId - The id of the selected provider
* @returns {Object} - An object mapping the name of inline form inputs
* to their DOM element
*/
_getInlineFormInputs: function (providerId) {
return {
sepa_dd_iban: document.getElementById(`o_sepa_dd_iban_${providerId}`),
sepa_dd_accept: document.getElementById(
`o_sepa_dd_accept_${providerId}`
),
};
},

/**
* Return values to pass to the default transaction validation
* route
*
* @private
* @param {String} code - The code of the payment option's provider
* @param {Number} paymentOptionId - The id of the selected provider
* @param {String} flow - The online payment flow of the transaction
* @returns {Object} - The params for the default transaction route
*/
_prepareTransactionRouteParams: function (code, paymentOptionId, flow) {
const values = this._super(code, paymentOptionId, flow);
const inputs = this._getInlineFormInputs(paymentOptionId);
values.sepa_dd_iban = inputs.sepa_dd_iban.value;
values.sepa_dd_accept = inputs.sepa_dd_accept.value;
return values;
},

/**
* Ensure that inputs for the form are valid (more validation
* are done in python by the backend).
*
* @override method from payment.payment_form_mixin
* @private
* @param {String} code - The code of the payment option's provider
* @param {Number} paymentOptionId - The id of the payment option handling the transaction
* @param {String} flow - The online payment flow of the transaction
* @returns {Promise}
*/
_processPayment: function (code, paymentOptionId, flow) {
if (code !== "custom" || flow === "token") {
return this._super(...arguments);
}

if (!this._validateFormInputs(paymentOptionId)) {
this._enableButton();
$("body").unblock();
return Promise.resolve();
}

return this._super(...arguments);
},

/**
* Checks that all payment inputs adhere to the DOM validation constraints.
*
* @private
* @param {Number} providerId - The id of the selected provider
* @returns {Boolean} - Whether all elements pass the validation constraints
*/
_validateFormInputs: function (providerId) {
const inputs = Object.values(this._getInlineFormInputs(providerId));
return inputs.every((element) => element.reportValidity());
},
};

checkoutForm.include(sepaDDMixin);
manageForm.include(sepaDDMixin);
});
Loading

0 comments on commit bd6635f

Please sign in to comment.