magento2 – How to save address custom attributes in orders?


Magento version: 2.3.5-p1

What I need: I need to add 3 custom fields for the customer addresses (I need it only for billing, but it’s ok if they’re also in the shipping address). Fields should be editable. I need those fields in the addresses section of the user dashboard and checkout form (customer address) in the frontend and I need it also in the customer address area in the backend (to be editable by the administrator, if needed). When an order is placed, the order address should include those 3 additional fields, if not empty. The address in the confirmation email should include those 3 custom attributes. Since I need to manually add some orders from the backend, those 3 custom address fields should work also while adding orders manually from the backend.

What I tried so far: I followed a series of tutorials on youtube (I’m not a magento developer and I haven’t found anything else). Links below. In these tutorials I was guided to create a module that adds custom attribute fields to customer addresses. Then I updated my address templates in the magento configuration to include the new fields.




https://www.youtube.com/watch?v=V8_3ZJ-IocE

Problem: Everything works fine in the customer address creation/edit, both frontend and backend. But the custom fields are not saved with the orders. Orders are placed but the address associated with it don’t include those 3 custom fields. This happens both on frontend and backend.


Here is the code of all files in the module.

app/code/Devchannel/CustomAttribute/registration.php

<?php
MagentoFrameworkComponentComponentRegistrar::register(
    MagentoFrameworkComponentComponentRegistrar::MODULE,
    'Devchannel_CustomAttribute',
    __DIR__
);

app/code/Devchannel/CustomAttribute/Block/Customer/Address/Form/Edit/CodiceFiscale.php

<?php

namespace DevchannelCustomAttributeBlockCustomerAddressFormEdit;

use MagentoCustomerApiAddressRepositoryInterface;
use MagentoCustomerApiDataAddressInterface;
use MagentoFrameworkExceptionNoSuchEntityException;
use MagentoFrameworkViewElementTemplate;
use MagentoCustomerApiDataAddressInterfaceFactory;
use MagentoCustomerModelSession;

class CodiceFiscale extends Template
{
    private $address;
    private $addressRepository;
    private $addressFactory;
    private $customerSession;

    public function __construct(
        TemplateContext $context,
        AddressRepositoryInterface $addressRepository,
        AddressInterfaceFactory $addressFactory,
        Session $session,
        array $data = ()
    ) {
        parent::__construct($context, $data);
        $this->addressRepository = $addressRepository;
        $this->addressFactory = $addressFactory;
        $this->customerSession = $session;
    }

    protected function _prepareLayout()
    {
        $addressId = $this->getRequest()->getParam('id');

        if($addressId) {
            try {
                $this->address = $this->addressRepository->getById($addressId);
                if ($this->address->getCustomerId() != $this->customerSession->getCustomerId()) {
                    $this->address = null;
                }
            } catch (NoSuchEntityException $exception) {
                $this->address = null;
            }
        }

        if (null === $this->address) {
            $this->address = $this->addressFactory->create();
        }

        return parent::_prepareLayout();
    }

    protected function _toHtml()
    {
        $customWidgetBlock = $this->getLayout()->createBlock(
            'DevchannelCustomAttributeBlockCustomerWidgetCodiceFiscale'
        );

        $customWidgetBlock->setAddress($this->address);
        return $customWidgetBlock->toHtml();
    }
}

app/code/Devchannel/CustomAttribute/Block/Customer/Address/Form/Edit/Pec.php

<?php

namespace DevchannelCustomAttributeBlockCustomerAddressFormEdit;

use MagentoCustomerApiAddressRepositoryInterface;
use MagentoCustomerApiDataAddressInterface;
use MagentoFrameworkExceptionNoSuchEntityException;
use MagentoFrameworkViewElementTemplate;
use MagentoCustomerApiDataAddressInterfaceFactory;
use MagentoCustomerModelSession;

class Pec extends Template
{
    private $address;
    private $addressRepository;
    private $addressFactory;
    private $customerSession;

    public function __construct(
        TemplateContext $context,
        AddressRepositoryInterface $addressRepository,
        AddressInterfaceFactory $addressFactory,
        Session $session,
        array $data = ()
    ) {
        parent::__construct($context, $data);
        $this->addressRepository = $addressRepository;
        $this->addressFactory = $addressFactory;
        $this->customerSession = $session;
    }

    protected function _prepareLayout()
    {
        $addressId = $this->getRequest()->getParam('id');

        if($addressId) {
            try {
                $this->address = $this->addressRepository->getById($addressId);
                if ($this->address->getCustomerId() != $this->customerSession->getCustomerId()) {
                    $this->address = null;
                }
            } catch (NoSuchEntityException $exception) {
                $this->address = null;
            }
        }

        if (null === $this->address) {
            $this->address = $this->addressFactory->create();
        }

        return parent::_prepareLayout();
    }

    protected function _toHtml()
    {
        $customWidgetBlock = $this->getLayout()->createBlock(
            'DevchannelCustomAttributeBlockCustomerWidgetPec'
        );

        $customWidgetBlock->setAddress($this->address);
        return $customWidgetBlock->toHtml();
    }
}

app/code/Devchannel/CustomAttribute/Block/Customer/Address/Form/Edit/Sdi.php

<?php

namespace DevchannelCustomAttributeBlockCustomerAddressFormEdit;

use MagentoCustomerApiAddressRepositoryInterface;
use MagentoCustomerApiDataAddressInterface;
use MagentoFrameworkExceptionNoSuchEntityException;
use MagentoFrameworkViewElementTemplate;
use MagentoCustomerApiDataAddressInterfaceFactory;
use MagentoCustomerModelSession;

class Sdi extends Template
{
    private $address;
    private $addressRepository;
    private $addressFactory;
    private $customerSession;

    public function __construct(
        TemplateContext $context,
        AddressRepositoryInterface $addressRepository,
        AddressInterfaceFactory $addressFactory,
        Session $session,
        array $data = ()
    ) {
        parent::__construct($context, $data);
        $this->addressRepository = $addressRepository;
        $this->addressFactory = $addressFactory;
        $this->customerSession = $session;
    }

    protected function _prepareLayout()
    {
        $addressId = $this->getRequest()->getParam('id');

        if($addressId) {
            try {
                $this->address = $this->addressRepository->getById($addressId);
                if ($this->address->getCustomerId() != $this->customerSession->getCustomerId()) {
                    $this->address = null;
                }
            } catch (NoSuchEntityException $exception) {
                $this->address = null;
            }
        }

        if (null === $this->address) {
            $this->address = $this->addressFactory->create();
        }

        return parent::_prepareLayout();
    }

    protected function _toHtml()
    {
        $customWidgetBlock = $this->getLayout()->createBlock(
            'DevchannelCustomAttributeBlockCustomerWidgetSdi'
        );

        $customWidgetBlock->setAddress($this->address);
        return $customWidgetBlock->toHtml();
    }
}

app/code/Devchannel/CustomAttribute/Block/Customer/Widget/CodiceFiscale.php

<?php

namespace DevchannelCustomAttributeBlockCustomerWidget;

use MagentoCustomerApiAddressMetadataInterface;
use MagentoCustomerApiDataAddressInterface;
use MagentoFrameworkViewElementTemplate;

class CodiceFiscale extends Template
{
    private $addressMetadata;

    public function __construct(
        TemplateContext $context,
        AddressMetadataInterface $addressMetadata,
        array $data = ()
    ) {
        parent::__construct($context, $data);
        $this->addressMetadata = $addressMetadata;
    }

    protected function _construct()
    {
        parent::_construct();
        $this->setTemplate('widget/codice_fiscale.phtml');
    }

    public function isRequired()
    {
        return $this->getAttribute()
            ? $this->getAttribute()->isRequired()
            : false;
    }

    public function getFieldId()
    {
        return 'codice_fiscale';
    }

    public function getFieldLabel()
    {
        return $this->getAttribute()
            ? $this->getAttribute()->getFrontendLabel()
            : __('Tax ID');
    }

    public function getFieldName()
    {
        return 'codice_fiscale';
    }

    public function getValue()
    {
        $address = $this->getAddress();
        if ($address instanceof AddressInterface) {
            return $address->getCustomAttribute('codice_fiscale')
                ? $address->getCustomAttribute('codice_fiscale')->getValue()
                : null;
        }
        return null;
    }

    private function getAttribute()
    {
        try {
            $attribute = $this->addressMetadata->getAttributeMetadata('codice_fiscale');
        } catch (NoSuchEntityException $exception) {
            return null;
        }

        return $attribute;
    }
}

app/code/Devchannel/CustomAttribute/Block/Customer/Widget/Pec.php

<?php

namespace DevchannelCustomAttributeBlockCustomerWidget;

use MagentoCustomerApiAddressMetadataInterface;
use MagentoCustomerApiDataAddressInterface;
use MagentoFrameworkViewElementTemplate;

class Pec extends Template
{
    private $addressMetadata;

    public function __construct(
        TemplateContext $context,
        AddressMetadataInterface $addressMetadata,
        array $data = ()
    ) {
        parent::__construct($context, $data);
        $this->addressMetadata = $addressMetadata;
    }

    protected function _construct()
    {
        parent::_construct();
        $this->setTemplate('widget/pec.phtml');
    }

    public function isRequired()
    {
        return $this->getAttribute()
            ? $this->getAttribute()->isRequired()
            : false;
    }

    public function getFieldId()
    {
        return 'pec';
    }

    public function getFieldLabel()
    {
        return $this->getAttribute()
            ? $this->getAttribute()->getFrontendLabel()
            : __('Certified e-mail address');
    }

    public function getFieldName()
    {
        return 'pec';
    }

    public function getValue()
    {
        $address = $this->getAddress();
        if ($address instanceof AddressInterface) {
            return $address->getCustomAttribute('pec')
                ? $address->getCustomAttribute('pec')->getValue()
                : null;
        }
        return null;
    }

    private function getAttribute()
    {
        try {
            $attribute = $this->addressMetadata->getAttributeMetadata('pec');
        } catch (NoSuchEntityException $exception) {
            return null;
        }

        return $attribute;
    }
}

app/code/Devchannel/CustomAttribute/Block/Customer/Widget/Sdi.php

<?php

namespace DevchannelCustomAttributeBlockCustomerWidget;

use MagentoCustomerApiAddressMetadataInterface;
use MagentoCustomerApiDataAddressInterface;
use MagentoFrameworkViewElementTemplate;

class Sdi extends Template
{
    private $addressMetadata;

    public function __construct(
        TemplateContext $context,
        AddressMetadataInterface $addressMetadata,
        array $data = ()
    ) {
        parent::__construct($context, $data);
        $this->addressMetadata = $addressMetadata;
    }

    protected function _construct()
    {
        parent::_construct();
        $this->setTemplate('widget/sdi.phtml');
    }

    public function isRequired()
    {
        return $this->getAttribute()
            ? $this->getAttribute()->isRequired()
            : false;
    }

    public function getFieldId()
    {
        return 'sdi';
    }

    public function getFieldLabel()
    {
        return $this->getAttribute()
            ? $this->getAttribute()->getFrontendLabel()
            : __('Unique code (SDI)');
    }

    public function getFieldName()
    {
        return 'sdi';
    }

    public function getValue()
    {
        $address = $this->getAddress();
        if ($address instanceof AddressInterface) {
            return $address->getCustomAttribute('sdi')
                ? $address->getCustomAttribute('sdi')->getValue()
                : null;
        }
        return null;
    }

    private function getAttribute()
    {
        try {
            $attribute = $this->addressMetadata->getAttributeMetadata('sdi');
        } catch (NoSuchEntityException $exception) {
            return null;
        }

        return $attribute;
    }
}

app/code/Devchannel/CustomAttribute/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="Devchannel_CustomAttribute" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Customer" />
        </sequence>
    </module>
</config>

app/code/Devchannel/CustomAttribute/etc/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="MagentoCustomerApiDataAddressInterface">
        <attribute code="codice_fiscale" type="string" />
        <attribute code="pec" type="string" />
        <attribute code="sdi" type="string" />
    </extension_attributes>
</config>

app/code/Devchannel/CustomAttribute/etc/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="MagentoCustomerBlockAddressEdit">
        <plugin name="DevchannelCustomAttributeCustomerAddressEditPlugin" type="DevchannelCustomAttributePluginCustomerAddressEditPlugin" />
    </type>
</config>

app/code/Devchannel/CustomAttribute/Plugin/Customer/AddressEditPlugin.php

<?php

namespace DevchannelCustomAttributePluginCustomer;

use MagentoFrameworkViewLayoutInterface;

class AddressEditPlugin
{
    private $layout;

    public function __construct(
        LayoutInterface $layout
    ) {
        $this->layout = $layout;
    }

    public function afterGetNameBlockHtml(
        MagentoCustomerBlockAddressEdit $edit,
        $result
    ) {
        $customBlock1 = $this->layout->createBlock('DevchannelCustomAttributeBlockCustomerAddressFormEditCodiceFiscale', 'devchannel_custom_attribute_cf');
        $customBlock2 = $this->layout->createBlock('DevchannelCustomAttributeBlockCustomerAddressFormEditPec', 'devchannel_custom_attribute_pec');
        $customBlock3 = $this->layout->createBlock('DevchannelCustomAttributeBlockCustomerAddressFormEditSdi', 'devchannel_custom_attribute_sdi');
        return $result . $customBlock1->toHtml() . $customBlock2->toHtml() . $customBlock3->toHtml();
    }
}

app/code/Devchannel/CustomAttribute/Setup/InstallData.php

<?php

namespace DevchannelCustomAttributeSetup;

use MagentoCustomerApiAddressMetadataInterface;
use MagentoEavSetupEavSetup;
use MagentoFrameworkSetupInstallDataInterface;
use MagentoFrameworkSetupModuleContextInterface;
use MagentoFrameworkSetupModuleDataSetupInterface;
use MagentoEavModelConfig;

class InstallData implements InstallDataInterface
{
    const CUSTOM_ATTRIBUTE_CODE_CF = 'codice_fiscale';
    const CUSTOM_ATTRIBUTE_CODE_PEC = 'pec';
    const CUSTOM_ATTRIBUTE_CODE_SDI = 'sdi';

    private $eavSetup;
    private $eavConfig;

    public function __construct(EavSetup $eavSetup, Config $eavConfig)
    {
        $this->eavSetup = $eavSetup;
        $this->eavConfig = $eavConfig;
    }

    public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
    {
        $setup->startSetup();

        $this->eavSetup->addAttribute(
            AddressMetadataInterface::ENTITY_TYPE_ADDRESS,
            self::CUSTOM_ATTRIBUTE_CODE_CF,
            (
                'label' => 'Tax ID',
                'input' => 'text',
                'visible' => true,
                'required' => true,
                'position' => 150,
                'sort_order' => 150,
                'system' => false
            )
        );

        $codice_fiscale = $this->eavConfig->getAttribute(
            AddressMetadataInterface::ENTITY_TYPE_ADDRESS,
            self::CUSTOM_ATTRIBUTE_CODE_CF
        );

        $codice_fiscale->setData(
            'used_in_forms',
            ('adminhtml_customer_address', 'customer_address_edit', 'customer_register_address')
        );

        $codice_fiscale->save();

        $this->eavSetup->addAttribute(
            AddressMetadataInterface::ENTITY_TYPE_ADDRESS,
            self::CUSTOM_ATTRIBUTE_CODE_PEC,
            (
                'label' => 'Certified e-mail address',
                'input' => 'text',
                'visible' => true,
                'required' => false,
                'position' => 151,
                'sort_order' => 151,
                'system' => false
            )
        );

        $pec = $this->eavConfig->getAttribute(
            AddressMetadataInterface::ENTITY_TYPE_ADDRESS,
            self::CUSTOM_ATTRIBUTE_CODE_PEC
        );

        $pec->setData(
            'used_in_forms',
            ('adminhtml_customer_address', 'customer_address_edit', 'customer_register_address')
        );

        $pec->save();

        $this->eavSetup->addAttribute(
            AddressMetadataInterface::ENTITY_TYPE_ADDRESS,
            self::CUSTOM_ATTRIBUTE_CODE_SDI,
            (
                'label' => 'Unique code (SDI)',
                'input' => 'text',
                'visible' => true,
                'required' => false,
                'position' => 152,
                'sort_order' => 152,
                'system' => false
            )
        );

        $sdi = $this->eavConfig->getAttribute(
            AddressMetadataInterface::ENTITY_TYPE_ADDRESS,
            self::CUSTOM_ATTRIBUTE_CODE_SDI
        );

        $sdi->setData(
            'used_in_forms',
            ('adminhtml_customer_address', 'customer_address_edit', 'customer_register_address')
        );

        $sdi->save();

        $setup->endSetup();
    }
}

app/code/Devchannel/CustomAttribute/view/frontend/templates/widget/codice_fiscale.phtml

<!--aggiunto campo codice fiscale-->
<div class="field field-<?php echo $block->escapeHtml($block->getFieldName()); ?> <?php if ($block->isRequired()) echo 'required'; ?>">
    <label class="label" for="<?php echo $block->escapeHtml($block->getFieldId()); ?>"><span><?php echo $block->escapeHtml(__($block->getFieldLabel())); ?> <?= $block->escapeHtml(__('(Mandatory for tax purposes)')); ?></span></label>
    <?php
        $objectManager = MagentoFrameworkAppObjectManager::getInstance();
        $store = $objectManager->get('MagentoFrameworkLocaleResolver');
        if ($store->getLocale() != 'it_IT') {
            ?>
            <p style="font-size: 0.75em; color: red;"><?= $block->escapeHtml(__('We are requiring your tax ID, to enter on the invoice in order to facilitate the customs clearence. If, for any reason, you prefer to not provide with your ID, you may enter any number, even a single digit so the system will allow you to go ahead.')); ?></p>
            <?php
        }
    ?>
    <div class="control">
        <input type="text" id="<?php echo $block->escapeHtml($block->getFieldId()); ?>" name="<?php echo $block->escapeHtml($block->getFieldName()); ?>" value="<?php echo $block->escapeHtml($block->getValue()); ?>" title="<?php echo $block->escapeHtml(__($block->getFieldLabel())); ?>" class="input-text" <?php echo $block->getFieldParams(); ?> <?php if ($block->isRequired()) echo 'data-validate="{required:true}"'; ?>>
    </div>
</div>
<!--/FINE aggiunto campo codice fiscale-->

app/code/Devchannel/CustomAttribute/view/frontend/templates/widget/pec.phtml

<!--aggiunto campo pec-->
<div class="field field-<?php echo $block->escapeHtml($block->getFieldName()); ?> <?php if ($block->isRequired()) echo 'required'; ?>">
    <label class="label" for="<?php echo $block->escapeHtml($block->getFieldId()); ?>"><span><?php echo $block->escapeHtml(__($block->getFieldLabel())); ?></span></label>
    <div class="control">
        <input type="text" id="<?php echo $block->escapeHtml($block->getFieldId()); ?>" name="<?php echo $block->escapeHtml($block->getFieldName()); ?>" value="<?php echo $block->escapeHtml($block->getValue()); ?>" title="<?php echo $block->escapeHtml(__($block->getFieldLabel())); ?>" class="input-text" <?php echo $block->getFieldParams(); ?> <?php if ($block->isRequired()) echo 'data-validate="{required:true}"'; ?>>
    </div>
</div>
<!--/FINE aggiunto campo pec-->

app/code/Devchannel/CustomAttribute/view/frontend/templates/widget/sdi.phtml

<!--aggiunto campo sdi-->
<div class="field field-<?php echo $block->escapeHtml($block->getFieldName()); ?> <?php if ($block->isRequired()) echo 'required'; ?>">
    <label class="label" for="<?php echo $block->escapeHtml($block->getFieldId()); ?>"><span><?php echo $block->escapeHtml(__($block->getFieldLabel())); ?></span></label>
    <div class="control">
        <input type="text" id="<?php echo $block->escapeHtml($block->getFieldId()); ?>" name="<?php echo $block->escapeHtml($block->getFieldName()); ?>" value="<?php echo $block->escapeHtml($block->getValue()); ?>" title="<?php echo $block->escapeHtml(__($block->getFieldLabel())); ?>" class="input-text" <?php echo $block->getFieldParams(); ?> <?php if ($block->isRequired()) echo 'data-validate="{required:true}"'; ?>>
    </div>
</div>
<!--/FINE aggiunto campo sdi-->