Nello sviluppo di progetti Magento 2 una domanda ricorrente è quella di poter aggiungere una validazione del codice fiscale e della partita iva italiana. Bisogna infatti considerare che con l’introduzione della fatturazione elettronica, la richiesta del campo partita iva italiano e codice fiscale è sempre più frequente.

Se non si vuole perdere tempo nella programmazione, segnalo un ottimo modulo per Magento 2 per la gestione della fatturazione elettronica.

Se invece si vuole creare un modulo semplice che inserire i campi codice fiscale e partita iva si possiamo seguire questi passi. Supponiamo di voler creare due nuovi attributi dell’indirizzo del customer e chiamarli partita_iva e codice_fiscale.

Creazione di un nuovo modulo

Per fare questo dobbiamo prima di tutto creare un modulo e inserire i nuovi attributi. La tabella che gestisce gli attributi anche in Magento 2 è la eav_attribute.

I campi per noi interessanti sono is_user_defined che deve essere posto a 1 e frontend_class che conterrà il nome della nostra classe di validazione. Per le classi di validazione sceglieremo i nomi validate-parita-iva e validate-code-fiscale.

Creiamo prima di tutto il nostro modulo. Il nome del modulo è EDRU_ItalianFiscalCode

La struttura del nostro modulo sarà la seguente:

Vediamo il codice nel dettaglio. Come sappiamo i file principali per creare un nuovo modulo sono registration.php e etc/module.xml e composer.js (vedi la documentazione ufficiale).

registration.php

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'EDRU_ItalianFiscalCode',
    __DIR__
);

etc/module.xml

<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
	<module name="EDRU_ItalianFiscalCode" setup_version="1.0.0">
		<sequence>
			<module name="Magento_Customer"/>
			<module name="Magento_Checkout"/>
		</sequence>
	</module>
</config>

(per chi usa PhpStorm e vuole avere una risoluzione degli urn può usare questo comando da console : bin/magento dev:urn-catalog:generate .idea/misc.xml per un approfondimento si rimanda all’articolo di Alan Kent consultabile qui).

composer.json

{
    "name": "edru/module-italianfiscalcode",
    "description": "Modulo per la gestione della partita iva e del codice fiscale",
    "type": "magento2-module",
    "license": "GPL-3.0",
    "authors": [
        {
            "name": "Mage2Gen",
            "email": "info@mage2gen.com"
        }
    ],
    "minimum-stability": "dev",
    "require": {},
    "autoload": {
        "psr-4": {
            "EDRU\\ItalianFiscalCode\\": ""
        },
        "files": [
            "registration.php"
        ]
    }
}

Installazione dei nuovi campi partita iva e codice fiscale

Il prossimo passo è quello di aggiungere i nuovi campi nell’installazione. I file di installazione in Magento 2 sono posizionati nella cartella setup. Il nostro modulo utilizza la classe \Magento\Customer\Setup\CustomerSetupFactory per creare una classe di tipo \Magento\Customer\Setup\CustomerSetup e sfruttare il metodo addAttribute per aggiungere l’attributo.

/**
     * {@inheritdoc}
     */
    public function install(
        ModuleDataSetupInterface $setup,
        ModuleContextInterface $context
    )
    {
        /** @var \Magento\Customer\Setup\CustomerSetup $customerSetup */
        $customerSetup = $this->customerSetupFactory->create(['setup' => $setup]);

        $customerSetup->addAttribute(\Magento\Customer\Model\Indexer\Address\AttributeProvider::ENTITY, 'partita_iva', [
            'label' => 'partita_iva',
            'input' => 'text',
            'type' => 'varchar',
            'source' => '',
            'required' => true,
            'position' => 333,
            'visible' => true,
            'system' => false,
            'is_used_in_grid' => false,
            'is_visible_in_grid' => false,
            'is_filterable_in_grid' => false,
            'is_searchable_in_grid' => false,
            'backend' => '',
            'frontend_class' => 'validate-codice-partita-iva'
        ]);

        $attribute = $customerSetup->getEavConfig()->getAttribute('customer_address', 'partita_iva')
            ->addData(['used_in_forms' => [
                'adminhtml_customer_address',
                'customer_address_edit',
                'customer_register_address'
            ]
            ]);
        $attribute->save();

        $customerSetup->addAttribute(\Magento\Customer\Model\Indexer\Address\AttributeProvider::ENTITY, 'codice_fiscale', [
            'label' => 'codice_fiscale',
            'input' => 'text',
            'type' => 'varchar',
            'source' => '',
            'required' => true,
            'position' => 333,
            'visible' => true,
            'system' => false,
            'is_used_in_grid' => false,
            'is_visible_in_grid' => false,
            'is_filterable_in_grid' => false,
            'is_searchable_in_grid' => false,
            'backend' => '',
            'frontend_class' => 'validate-codice-fiscale'
        ]);

        $attribute = $customerSetup->getEavConfig()->getAttribute('customer_address', 'codice_fiscale')
            ->addData(['used_in_forms' => [
                'adminhtml_customer_address',
                'customer_address_edit',
                'customer_register_address'
            ]
            ]);
        $attribute->save();
    }

la chiave ‘frontend_class’ => ‘validate-codice-fiscale’ aggiunge il valore validate-codice-fiscale alla colonna frontend_class della tabella eav_attribute.

Il valore required => ‘true’ renderà i nostri attributi obbligatori. Se lanciamo il comando php bin/magento setup:upgrade, il modulo crea i nuovi attributi nel database con i dati specificati sopra.

Magento 2 utilizza il concetto di extended attribute per la gestione degli attributi estesi del customer. La prima cosa da fare è definirli in un file xml denominato extension_attributes.xml

<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
	<extension_attributes for="Magento\Customer\Api\Data\AddressInterface">
		<attribute code="partita_iva" type="string"/>
		<attribute code="codice_fiscale" type="string"/>
	</extension_attributes>
</config>

Gestione del salvataggio degli attributi

La gestione degli attributi e del salvataggio deve essere fatto dal programmatore. E’ quindi necessario definire due plug in uno per il billing e l’altro per lo shipping. Per definire i plug in è necessario inserirne la definizione nel file di.xml

<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
	<type name="Magento\Quote\Model\ShippingAddressManagement">
		<plugin disabled="false" name="EDRU_ItalianFiscalCode_Plugin_Magento_Quote_Model_ShippingAddressManagement" sortOrder="10" type="EDRU\ItalianFiscalCode\Plugin\Magento\Quote\Model\ShippingAddressManagement"/>
	</type>
	<type name="Magento\Quote\Model\BillingAddressManagement">
		<plugin disabled="false" name="EDRU_ItalianFiscalCode_Plugin_Magento_Quote_Model_BillingAddressManagement" sortOrder="10" type="EDRU\ItalianFiscalCode\Plugin\Magento\Quote\Model\BillingAddressManagement"/>
	</type>
</config>

I due plug-in di tipo before sono agganciati al metodo Assign.

<?php
/**
 * Modulo per la gestione della partita iva e del codice fiscale
 * Copyright (C) 2019  
 * 
 * This file is part of EDRU/ItalianFiscalCode.
 * 
 * EDRU/ItalianFiscalCode is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

namespace EDRU\ItalianFiscalCode\Plugin\Magento\Quote\Model;

class BillingAddressManagement
{

    /**
     * @param \Magento\Quote\Model\BillingAddressManagement $subject
     * @param $cartId
     * \Magento\Quote\Api\Data\AddressInterface $address
     * @return array
     */
    public function beforeAssign(
        \Magento\Quote\Model\BillingAddressManagement $subject,
        $cartId,
        \Magento\Quote\Api\Data\AddressInterface $address
    ) {
        $extAttributes = $address->getExtensionAttributes();
        if (!empty($extAttributes)) {
            try {
                $address->setPartitaIva($extAttributes->getPartitaIva());

                $address->setCodiceFiscale($extAttributes->getCodiceFiscale());

            } catch (\Exception $e) {
            }
        }
        return [$cartId, $address];
    }
}

Assegnano il valore ai nuovi campi creati nel modulo di installazione. Questa parte si occupa di eseguire il salvataggio dei dati nel processo di checkout.

Gestione della validazione sul frontend

Per quanto riguarda invece la parte di fronend, dobbiamo intervenire nel require-js e nel blocco di layout processor. Vediamo prima il blocco LayoutProcessor.

<?php
/**
 * Modulo per la gestione della partita iva e del codice fiscale
 * Copyright (C) 2019
 *
 * This file is part of EDRU/ItalianFiscalCode.
 *
 * EDRU/ItalianFiscalCode is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

namespace EDRU\ItalianFiscalCode\Block\Checkout;

class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcessorInterface
{

    public function getShippingFormFields($result)
    {
        if (isset($result['components']['checkout']['children']['steps']['children']
            ['shipping-step']['children']['shippingAddress']['children']
            ['shipping-address-fieldset'])
        ) {
            $customShippingFields = $this->getFields('shippingAddress.custom_attributes', 'shipping');

            $shippingFields = $result['components']['checkout']['children']['steps']['children']
            ['shipping-step']['children']['shippingAddress']['children']
            ['shipping-address-fieldset']['children'];

            $shippingFields = array_replace_recursive($shippingFields, $customShippingFields);

            $result['components']['checkout']['children']['steps']['children']
            ['shipping-step']['children']['shippingAddress']['children']
            ['shipping-address-fieldset']['children'] = $shippingFields;
        }

        return $result;
    }

    public function getBillingFormFields($result)
    {
        if (isset($result['components']['checkout']['children']['steps']['children']
            ['billing-step']['children']['payment']['children']
            ['payments-list'])
        ) {
            $paymentForms = $result['components']['checkout']['children']['steps']['children']
            ['billing-step']['children']['payment']['children']
            ['payments-list']['children'];

            foreach ($paymentForms as $paymentMethodForm => $paymentMethodValue) {

                $paymentMethodCode = str_replace('-form', '', $paymentMethodForm);

                if (!isset($result['components']['checkout']['children']['steps']['children']['billing-step']['children']['payment']['children']['payments-list']['children'][$paymentMethodCode . '-form'])) {
                    continue;
                }

                $billingFields = $result['components']['checkout']['children']['steps']['children']
                ['billing-step']['children']['payment']['children']
                ['payments-list']['children'][$paymentMethodCode . '-form']['children']['form-fields']['children'];

                $customBillingFields = $this->getFields('billingAddress' . $paymentMethodCode . '.custom_attributes', 'billing');

                $billingFields = array_replace_recursive($billingFields, $customBillingFields);

                $result['components']['checkout']['children']['steps']['children']
                ['billing-step']['children']['payment']['children']
                ['payments-list']['children'][$paymentMethodCode . '-form']['children']['form-fields']['children'] = $billingFields;
            }
        }

        return $result;
    }

    public function process($result)
    {
        $result = $this->getShippingFormFields($result);
        $result = $this->getBillingFormFields($result);

        return $result;
    }

    public function getFields($scope, $addressType)
    {
        $fields = [];
        foreach ($this->getAdditionalFields($addressType) as $field) {
            $fields[$field] = $this->getField($field, $scope);
        }

        return $fields;
    }

    public function getField($attributeCode, $scope)
    {
        $field = [
            'config' => [
                'customScope' => $scope,
                'template' => 'ui/form/field',
                'elementTmpl' => 'ui/form/element/input'
            ],
            'dataScope' => $scope . '.' . $attributeCode,
            'sortOrder' => '333',
            'visible' => true,
            'provider' => 'checkoutProvider',
            'validation' => [],
            'options' => ['validate-codice-fiscale'],
            'label' => __('codice_fiscale')
        ];
        if ($attributeCode == 'partita_iva') {
            $field = [
                'config' => [
                    'customScope' => $scope,
                    'template' => 'ui/form/field',
                    'elementTmpl' => 'ui/form/element/input'
                ],
                'dataScope' => $scope . '.' . $attributeCode,
                'sortOrder' => '333',
                'visible' => true,
                'provider' => 'checkoutProvider',
                'validation' => ['validate-partita-iva'],
                'options' => [],
                'label' => __('partita_iva')
            ];
        }
        

        return $field;
    }

    public function getAdditionalFields($addressType = 'shipping')
    {
        $shippingAttributes = [];
        $billingAttributes = [];
        $shippingAttributes[] = 'partita_iva';
        $billingAttributes[] = 'partita_iva';


        $shippingAttributes[] = 'codice_fiscale';
        $billingAttributes[] = 'codice_fiscale';

        return $addressType == 'shipping' ? $shippingAttributes : $billingAttributes;
    }
}

Questo blocco viene passato come argomento alla classe Magento\Checkout\Block\Onepage. La definizione dell’argomento è fatto nel file di configurazione frontend\di.xml

<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Checkout\Block\Onepage">
        <arguments>
            <argument name="layoutProcessors" xsi:type="array">
                <item name="edru_extra_checkout_address_fields_layoutprocessor"
                      xsi:type="object">EDRU\ItalianFiscalCode\Block\Checkout\LayoutProcessor
                </item>
            </argument>
        </arguments>
    </type>
</config>

I metodi fondamentali per la visualizzazione dei campi sono:

public function getField($attributeCode, $scope)
    {
        $field = [
            'config' => [
                'customScope' => $scope,
                'template' => 'ui/form/field',
                'elementTmpl' => 'ui/form/element/input'
            ],
            'dataScope' => $scope . '.' . $attributeCode,
            'sortOrder' => '333',
            'visible' => true,
            'provider' => 'checkoutProvider',
            'validation' => [],
            'options' => ['validate-codice-fiscale'],
            'label' => __('codice_fiscale')
        ];
        if ($attributeCode == 'partita_iva') {
            $field = [
                'config' => [
                    'customScope' => $scope,
                    'template' => 'ui/form/field',
                    'elementTmpl' => 'ui/form/element/input'
                ],
                'dataScope' => $scope . '.' . $attributeCode,
                'sortOrder' => '333',
                'visible' => true,
                'provider' => 'checkoutProvider',
                'validation' => ['validate-partita-iva'],
                'options' => [],
                'label' => __('partita_iva')
            ];
        }
        

        return $field;
    }

    public function getAdditionalFields($addressType = 'shipping')
    {
        $shippingAttributes = [];
        $billingAttributes = [];
        $shippingAttributes[] = 'partita_iva';
        $billingAttributes[] = 'partita_iva';


        $shippingAttributes[] = 'codice_fiscale';
        $billingAttributes[] = 'codice_fiscale';

        return $addressType == 'shipping' ? $shippingAttributes : $billingAttributes;
    }

La parte finale del modulo è la visualizzazione tramite js. I file per la gestione dei campi e delle validazioni sono i seguenti:

Struttura js per validazione e gestione dei campi partita iva e codice fiscale.

Vediamo il file requirejs-config.js. Il file requirejs-config.js viene utilizzato da magento per gestire le risorse js. (per approfondire l’argomento utilizzare questo link).

var config = {
    config: {
        mixins: {
            'Magento_Checkout/js/action/set-billing-address': {
                'EDRU_ItalianFiscalCode/js/action/set-billing-address-mixin': true
            },
            'Magento_Checkout/js/action/set-shipping-information': {
                'EDRU_ItalianFiscalCode/js/action/set-shipping-information-mixin': true
            },
            'Magento_Checkout/js/action/create-shipping-address': {
                'EDRU_ItalianFiscalCode/js/action/create-shipping-address-mixin': true
            },
            'Magento_Checkout/js/action/place-order': {
                'EDRU_ItalianFiscalCode/js/action/set-billing-address-mixin': true
            },
            'Magento_Checkout/js/action/create-billing-address': {
                'EDRU_ItalianFiscalCode/js/action/set-billing-address-mixin': true
            },
            'Magento_Ui/js/lib/validation/validator': {
                'MG_CustomerAddress/js/validator-mixin': true
            }
        }
    }
};

In questo caso viene utilizzata la parola chiave mixin che esegue una fusione del codice della classe a sinistra con quella a destra.

'Magento_Checkout/js/action/create-shipping-address': {
                'EDRU_ItalianFiscalCode/js/action/create-shipping-address-mixin': true

Il codice di EDRU_ItalianFiscalCode/js/action/create-shipping-address-mixin.js viene incorporato in Magento_Checkout/js/action/create-shipping-address.js.

create-shipping-address-mixin.js

define([
    'jquery',
    'mage/utils/wrapper',
    'Magento_Checkout/js/model/quote'
], function ($, wrapper,quote) {
    'use strict';

    return function (setShippingInformationAction) {
        return wrapper.wrap(setShippingInformationAction, function (originalAction, messageContainer) {

            if (messageContainer.custom_attributes != undefined) {
                $.each(messageContainer.custom_attributes , function( key, value ) {
                    messageContainer['custom_attributes'][key] = {'attribute_code':key,'value':value};
                });
            }

            return originalAction(messageContainer);
        });
    };
});

set-billing-address-mixin.js

define([
    'jquery',
    'mage/utils/wrapper',
    'Magento_Checkout/js/model/quote'
], function ($, wrapper,quote) {
    'use strict';

    return function (setBillingAddressAction) {
        return wrapper.wrap(setBillingAddressAction, function (originalAction, messageContainer) {

            var billingAddress = quote.billingAddress();

            if(billingAddress != undefined) {

                if (billingAddress['extension_attributes'] === undefined) {
                    billingAddress['extension_attributes'] = {};
                }

                if (billingAddress.customAttributes != undefined) {
                    $.each(billingAddress.customAttributes, function (key, value) {

                        if($.isPlainObject(value)){
                            value = value['value'];
                        }

                        billingAddress['extension_attributes'][key] = value;
                    });
                }

            }

            return originalAction(messageContainer);
        });
    };
});

set-shipping-information-mixin.js

define([
    'jquery',
    'mage/utils/wrapper',
    'Magento_Checkout/js/model/quote'
], function ($, wrapper,quote) {
    'use strict';

    return function (setShippingInformationAction) {
        return wrapper.wrap(setShippingInformationAction, function (originalAction, messageContainer) {

            var shippingAddress = quote.shippingAddress();

            if (shippingAddress['extension_attributes'] === undefined) {
                shippingAddress['extension_attributes'] = {};
            }

            if (shippingAddress.customAttributes != undefined) {
                $.each(shippingAddress.customAttributes , function( key, value ) {

                    if($.isPlainObject(value)){
                        value = value['value'];
                    }

                    shippingAddress['customAttributes'][key] = value;
                    shippingAddress['extension_attributes'][key] = value;

                });
            }

            return originalAction(messageContainer);
        });
    };
});

L’ultimo js da incorporare è il nostro modulo di validazione. validator-mixin.js.

define([
    'jquery',
    'moment'
], function ($, moment) {
    'use strict';

    return function (validator) {

        validator.addRule(
            'validate-partita-iva',
            function (value, params, additionalParams) {
                if (value == '00000000000') {
                    return false;
                }
                if (!/^[0-9]{11}$/.test(value))
                    return false;
                let s = 0;
                for (let i = 0; i <= 9; i += 2)
                    s += value.charCodeAt(i) - '0'.charCodeAt(0);
                for (let i = 1; i <= 9; i += 2) {
                    let c = 2 * (value.charCodeAt(i) - '0'.charCodeAt(0));
                    if (c > 9) c = c - 9;
                    s += c;
                }
                let attended = (10 - s % 10) % 10;
                if (attended != value.charCodeAt(10) - '0'.charCodeAt(0)) {
                    return false;
                }

                return true;


            },
            $.mage.__("Partita iva non valida")
        );

        validator.addRule(
            'validate-codice-fiscale',
            function (value, params, additionalParams) {
                let cf = value;
                cf = cf.toUpperCase();
                if( !/^[0-9A-Z]{16}$/.test(cf) ){
                    return false;
                }
                let map = [1, 0, 5, 7, 9, 13, 15, 17, 19, 21, 1, 0, 5, 7, 9, 13, 15, 17,
                    19, 21, 2, 4, 18, 20, 11, 3, 6, 8, 12, 14, 16, 10, 22, 25, 24, 23];
                let s = 0;
                for(let i = 0; i < 15; i++){
                    let c = cf.charCodeAt(i);
                    if( c < 65 )
                        c = c - 48;
                    else
                        c = c - 55;
                    if( i % 2 == 0 )
                        s += map[c];
                    else
                        s += c < 10? c : c - 10;
                }
                let attended = String.fromCharCode(65 + s % 26);

                if( attended != cf.charAt(15) ){
                    return false;
                }
                return true;
            },
            $.mage.__("codice fiscale non valido")
        );

        return validator;
    };
});

Il modulo dovrebbe essere completo con la gestione del codice fiscale e della partita iva con una validazione custom.