Extension Attributes for Total Model in Magento 2

We continue to set extension attributes in Magento® 2. Last time we were talking about the adding a custom attribute to the customer’s address. But what if we need to set these attributes for the total model? Today we’ll consider how we can do that step by step.

Total Model

Total model is a dynamic system used for collecting the shopping cart and the order total. It usually contains the elements which take part in calculating the total cost: final products total, discount, tax, shipping fee, and some other parameters.

How It Works

Imagine the online store. You buy some products there and add them to the shopping cart. When you view the shopping cart page, you can see something like this:

Subtotal – X.XX$ final cost of products (the product’s cost multiplied by the number of products)

Tax  – X.XX$ (optionally)

Shipping  – X.XX$ (optionally)

Discount (optionally)

Total – X.XX $ (the final cost of the order)

Let’s take a look at VENROY online store (it’s Magento based) to see how it can work for you:

Magento based online store

The whole system shown in this example is the Total model. Let’s move on.

What We Need to Do

First of all, we need to add the row before subtotal in the total model. The row will show the customer’s profit. The profit is a marketing element that shows how much money the customer saves using the offer. This profit should not be included in the final cost of the shopping cart and the order totals. It is calculated on the base of subtotal.

So, we can separate our task into two main problems:

  • Add the row before Subtotal and don’t include its results to the total cost.
  • Make the extension and Magento 2 work right together.

These both conditions must work correctly with each other.

In the end, we should get the result of this kind:

You save: (Our additional row)

Subtotal – final cost of products (the product cost * the number of products)

Tax (optionally)

Shipping (optionally)

Discount (optionally)

Total – the final cost of the order

Let’s go on to code!

Preconditions

There is a Magento 2 based online store. It is oriented on the retail and wholesale customers. Due to this fact, most of the products have the tier prices, as well as the retail prices. The types of store products are the simple products and configurable products. As we said before, we need to output the additional row before Subtotal on the shopping cart and the order pages. This row must show the total profit for the wholesale customer. While calculating the profit, we don’t include the extra-charge for product options. This row mustn’t influence the order final cost, as well as it mustn’t be copied to the order.

Solution

Setting Magento 2 Extension Attributes

Last time, we added the custom attribute to the customer address. Let’s follow the same way. We’ll add the style_discount field with decimal(10,4) type to the address extension attributes table from the last example. Also, we’ll need to add the attribute to the extenson_attributes.xml file of our module. We should add it twice for two different interfaces.

...
<extension_attributes for="MagentoQuoteApiDataAddressInterface">
        <attribute code="type" type="int">
            <join reference_table="web4pro_quote_address" join_on_field="address_id" reference_field="address_id">
                <field column="type">type</field>
            </join>
        </attribute>
        <attribute code="style_discount" type="float">
            <join reference_table="web4pro_quote_address" join_on_field="address_id" reference_field="address_id">
                <field column="style_discount">style_discount</field>
            </join>
        </attribute>
    </extension_attributes>
    <extension_attributes for="MagentoQuoteApiDataTotalsInterface">
        <attribute code="style_discount" type="float"/>
    </extension_attributes>

In the first case, we should specify the table which will store this attribute. In the second case, there is no necessity to do this, because the entity implemented with MagentoQuoteApiDataTotalsInterface is always formed from the address data along the way. Saving of this attribute will be performed in sales_quote_address_save_after event processor, which we described in the previous example.

Furthermore, we need to describe the total model. Let’s create an etc/sales.xml file in the module for these purposes. It looks like the following:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Sales:etc/sales.xsd">
    <section name="quote">
        <group name="totals">
            <item name="style_discount" instance="Web4proAjaxcartModelQuoteAddressStyle" sort_order="110"/>
        </group>
    </section>
</config>

We choose the sort_order parameter keeping in mind that calculations were performed after Subtotal had been calculated. In this case, the Product entity has already been initialized for all shopping cart elements.

Implementing Total Model in Magento 2

The total model must implement two methods: collect() method used for the calculation and fetch() method used for the output on request. You can implement the total by following this example:

namespace Web4proAjaxcartModelQuoteAddress;
use MagentoQuoteModelQuoteAddressItem as AddressItem;

class Style extends MagentoQuoteModelQuoteAddressTotalAbstractTotal {

    protected $_objectManager;

    public function __construct(MagentoFrameworkObjectManagerInterface $objectManagerInterface){
        $this->_objectManager = $objectManagerInterface;
    }

    public function collect(
        MagentoQuoteModelQuote $quote,
        MagentoQuoteApiDataShippingAssignmentInterface $shippingAssignment,
        MagentoQuoteModelQuoteAddressTotal $total
    ) {
        parent::collect($quote,$shippingAssignment,$total);
        $items = $shippingAssignment->getItems();
        $address = $shippingAssignment->getShipping()->getAddress();
        $amount = 0;
        foreach($items as $item){
            if ($item->getParentItem()) {
                continue;
            }
            if ($item instanceof AddressItem) {
                $quoteItem = $item->getAddress()->getQuote()->getItemById($item
->getQuoteItemId());
            } else {
                $quoteItem = $item;
            }
            $product = $quoteItem->getData('product');
            $childProduct = $product;
            if($product->getTypeId()=='configurable'){
                $childProduct = $product->getCustomOption('simple_product')->getProduct();
            }
            $qty = $quoteItem->getQty();
            $amount =(float)($qty*($childProduct->getPriceModel()->getFinalPrice($qty,$childProduct)-$childProduct->getPriceModel()->getBasePrice($childProduct,1)));
        }
        $total->addTotalAmount($this->getCode(),$amount);
        $extensionAttr = $address->getExtensionAttributes();
        if(!$extensionAttr){
            $extensionAttr = $this->_objectManager
->create('MagentoQuoteApiDataAddressExtension');
        }
        $extensionAttr->setData($this->getCode(),$total->getTotalAmount($this->getCode()));
        $address->setExtensionAttributes($extensionAttr);
        return $this;
    }
    public function fetch(MagentoQuoteModelQuote $quote, MagentoQuoteModelQuoteAddressTotal $total)
    {
        $result = null;
        $amount = $total->getTotalAmount($this->getCode());

        if ($amount != 0) {
            $result = [
                'code' => $this->getCode(),
                'title' => __('Style Discount'),
                'value' => $amount
            ];
        }
        return $result;
    }
}

Copying Data to Total Model in Magento 2

We need to provide copying the data from the address to the total model. It’s possible with the plugin from the previous example to MagentoFrameworkApiDataObjectHelper class. But note that we should work separately with address attributes and total model attributes. We copy the first ones to the order address and the second ones – to the total model. When trying to copy some data to the receiver which can’t receive the attribute, the exception will be thrown out. The method looks like this one:

public function beforePopulateWithArray($helper,$dataObject, array $data, $interfaceName){
        switch($interfaceName){
            case 'MagentoSalesApiDataOrderAddressInterface':
                if($data['extension_attributes'] instanceof MagentoQuoteApiDataAddressExtensionInterface){
                    $data['extension_attributes'] = $data['extension_attributes']->__toArray();
                    if(isset($data['extension_attributes']['style_discount'])){
                        unset($data['extension_attributes']['style_discount']);
                    }
                }
            break;
            case 'MagentoCustomerApiDataAddressInterface':
                if(isset($data['extension_attributes'])&&($data['extension_attributes'] instanceof MagentoQuoteApiDataAddressExtensionInterface)){
                    $data['extension_attributes'] = $data['extension_attributes']->__toArray();
                    if(isset($data['extension_attributes']['type'])){
                        $data['type'] = $data['extension_attributes']['type'];
                    }
                }
                break;
            case 'MagentoQuoteApiDataTotalsInterface':
                if($data['extension_attributes'] instanceof MagentoFrameworkApiAbstractSimpleObject){
                    $data['extension_attributes'] = $data['extension_attributes']->__toArray();
                    if(isset($data['extension_attributes']['type'])){
                        unset($data['extension_attributes']['type']);
                    }
                }
                break;
        }
        return array($dataObject,$data,$interfaceName);
    }

Outputting Total on Shopping Cart Page

Now we are going to output a new Total on the shopping cart page. It requires us to describe and implement Magento 2 knockout-component. It’s described in Layout file the next way:

<referenceBlock name="checkout.cart.totals">
            <arguments>
                <argument name="jsLayout" xsi:type="array">
                    <item name="components" xsi:type="array">
                        <item name="block-totals" xsi:type="array">
                            <item name="children" xsi:type="array">
                                <item name="style_discount" xsi:type="array">
                                    <item name="component"  xsi:type="string">Web4pro_Ajaxcart/js/style</item>
                                    <item name="sortOrder" xsi:type="string">1</item>
                                    <item name="config" xsi:type="array">
                                        <item name="template" xsi:type="string">Web4pro_Ajaxcart/checkout/cart/totals/style</item>
                                        <item name="title" xsi:type="string" translate="true"><![CDATA[Style Quantity Discount]]></item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>

We choose sort_order taking into consideration that the row is outputted before Subtotal. As you can see, the component consists of a template and a javascript file. The template is loaded asynchronously, and it’s filled with the content with JavaScript. This is how the template supposed to look like:

<tr class="totals">
    <th class="mark" scope="row" data-bind="text: title"></th>
    <td data-bind="attr: {'data-th': title}" class="amount">
        <span class="price" data-bind="text: getValue()"></span>
    </td>
</tr>

JavaScript code:

define(
    [
        'Magento_Checkout/js/view/summary/abstract-total',
        'Magento_Checkout/js/model/quote'
    ],
    function (Component, quote) {
        "use strict";
        return Component.extend({

            isDisplayed: function() {
                return this.getPureValue()!=0;
            },
            getPureValue: function() {
                var totals = quote.getTotals()();
                if (totals) {
                    if(typeof totals.style_discount=='undefined'){
                        quote.setTotals(window.checkoutConfig.totalsData);
                        totals = quote.getTotals()();
                    }
                    return totals.style_discount;
                }
                return quote.style_discount;
            },
            getValue: function() {
                    return this.getFormattedPrice(this.getPureValue());
            }
         });
    }
);

Pay attention to the if condition (type of totals.style_discount==’undefined’). It’s necessary because Magento 2 extension attributes (we checked  2.1.2 version) are written to Total JavaScript object if only the asynchronous loading takes place (_SetTotals() method). It is not being written when the page is loaded from window.checkoutConfig.totalsData.

We can output this component to the order page by following the same routine.

Pitfalls

One of the moments we should keep in mind is that Total content is being added while calculating the Grand Total. It’s not good for our task. The reason lies in the implementation features of Grand Total calculation in MagentoQuoteModelQuoteAddressTotalGrand class. Take a look:

public function collect(
        MagentoQuoteModelQuote $quote,
        MagentoQuoteApiDataShippingAssignmentInterface $shippingAssignment,
        MagentoQuoteModelQuoteAddressTotal $total
    ) {
        $totals = array_sum($total->getAllTotalAmounts());
        $baseTotals = array_sum($total->getAllBaseTotalAmounts());

        $total->setGrandTotal($totals);
        $total->setBaseGrandTotal($baseTotals);
        return $this;
    }

We can fix this out using a plugin which excludes our total from the sum.

<type name="MagentoQuoteModelQuoteAddressTotal">
        <plugin name="web4pro-remove-external-totals" type="Web4proAjaxcartModelPlugin" sortOrder="20"/>
    </type>

public function afterGetAllTotalAmounts($total,$result){
        if(isset($result['style_discount'])){
            unset($result['style_discount']);
        }
        return $result;
    }

Conclusion

Finally, we have coped with our task. Let’s summarise what we needed to do:

  • add the style_discount field with decimal(10,4) type to the address extension attributes table;
  • add the attribute to the extenson_attributes.xml file of our module (twice for two different interfaces);
  • create the etc/sales.xml file in the module to describe the total model;
  • implement the total model;
  • provide copying the data from the address to the total model with the plugin (work separately with address attributes and total model attributes);
  • fix the adding of Total content while calculating the Grand Total with a special plugin;
  • output a new Total on the shopping cart and the order pages.

This is how it works. We hope that our article answers your question about set extension attributes for the total model. However, if you have some issues regarding extensions at your store and you need assistance, we can provide you with Magento 2 Extension Development.

Wish you good luck with Total Models in Magento 2!

How to Understand Clients: Step into Their Shoes

You’ve been working at the position of Project Manager for five years. You think you understand clients and all working processes. You follow the rule: “Think like a customer. Feel the pains of the customer”. But much as you tried, you are still the manager, who acts using your company’s working templates.

I have been working at WEB4PRO since 2012. We have succeeded to complete over 20 projects from scratch since then. Now I have taken a maternity leave. So, I’ve got the time for starting the own project. This is my story about how it was.

Step Into Client’s Shoes

My project is a medical website from scratch. I made up my mind to bring it to life, and I began with the preparation. First, I created mockups and short project requirements with the detailed description. Then I sent these data to WEB4PRO for the estimation, as I completely trust my team. I know they follow the timeline and provide the high-quality result.

But… “What if I look up some market offers? What about checking some other companies? Do they understand client’s business and needs?” – I thought.

So I got down to business.

Follow the Client’s Routine

I sent a lot of requests for quote. We started communication with 11 contacts. There were various executors: company representatives and freelancers. I prefer working with companies. The company could cover the risks when some unexpected problems arise. Furthermore, a company can provide a complex solution. I decided to compare all candidates by several aspects.

Proofs. Portfolio

We considered a lot of offers. We focused on two things along the way: portfolio and the quality of works there.

I evaluated the portfolio by the websites it included. The most criteria for we were the websites design, compatibility, mobile versions, complexity. Also, I tested the main features. I declined the companies if their portfolio contained bad-made websites. Those websites were too old, too simple, unattractive, inconvenient. But the high-quality works caught my eye even if it was not relevant to the medical field. The relevancy was not the main factor for me. If the company built a great website, it could do a good job for us. So, I came up with the following:

The example of work may not be from the client’s business area. That’s great if it has some features like the customer’s project does (even if it’s a contact form functionality). The good portfolio must consist of complicated, modern, and beautiful works.

First Interaction

First, I provided the companies with links to mockups and the project specification. I requested the project estimation, hourly rate, and a technical solution. I wanted to check their level of understanding the client’s requirements. The result was curious.

Three people gave the estimation right on the same day, on Saturday. According to WEB4PRO standards, we provide the estimation within 3 working days. I knew the work scope required for our project. That’s why costs and terms the candidates suggested looked strange for me. They seemed too short.

Many candidates revealed their hourly rate. Also, they provided the project duration expressed in months. So, I couldn’t understand the final cost. Project duration expressed in months doesn’t make any sense. It’s much better to express it in hours. When we multiply the project term on the hourly rate, we get the final cost.

In conclusion, I declined several candidates for the following reasons:

  • improbable estimation;
  • the absence of related questions;
  • inattentive learning of project requirements;
  • unclear estimation expressed in months;
  • low level of communication.

Most freelancers replied within two days. They all suggested we contact via Skype and discuss the detail.

Due to all these facts above, I made the next observation:

When you first interact with the client, you set the first contact. It’s very important to understand customers needs and wants right on the first stages. They’d like to hear the questions related to their project. Also, always give the project duration expressed in hours, not in months or something else. Costs must be easy to calculate. It shows your honesty, and it saves clients’ time.

Skype Call

I talked to five candidates. I didn’t like the ones who asked me the odd questions. I understood they were not ready for negotiations. Also, the candidates who didn’t have any creative approach were not the right people for my job. Definitely, they were not good at identifying customer needs.
 
The others were well-prepared. They asked the right questions and expressed the interest to my business. Some candidates even learned our competitors. One company showed the great approach to the work processes. Here is the process routine they follow with the clients:
  • 2 weekly email reports: 1 working-plan report, 1 results report;
  • Trello dashboard as a tool and how-to-use guide for customers;
  • 1 weekly Skype call;
  • key project manager for communication and the team members the client can refer to;
  • payment within the certain number of days after receiving the invoice.

This information made me more confident in that executor. So that, Skype call is one of the most important steps in building relationships with the customers. On this step, you can start discovering customer needs. I understood the following:

When we communicate by voice, we gain trust. Skype call is a good way to do that. Learn project requirements and speak in a friendly manner. Show that your understand the client’s problem. It deserves the respect.

Project Estimation

understand clients: provide clear reports

Project duration. The candidates determined the project term in months, more seldom – in weeks. Nobody provided me with the detailed estimation broken into small milestones. The most I’ve got was a description of the work scope broken into work processes. They were backend development, frontend development, web design, management, testing, and analysis. It didn’t help me to understand the final cost of project development.

Engagement models. Almost nobody wanted to use the Fixed Price model. The reason was the necessity of incorporating some extra tasks and edits along the way. The solution for that was the following. They offered to follow the Agile model with breaking the project into milestones. We had to pay for each milestone. This works well enough.

Candidates ready to work on fixed price required the approved high-fidelity prototypes. High-fidelity prototypes are interactive mockups allowing the simulation of user actions. In this case, your cooperation starts with creating such prototypes. And the company includes this works to invoice.

Business Proposal. Several candidates impressed me with their business proposal. It’s convenient to receive negotiations result packed in a beautiful business proposal. This way you can find out the main points without any efforts.

The business proposal consists of the following parts:
  • technical solution;
  • description of the project team;
  • project duration represented in graphics;
  • final cost;
  • payment terms;
  • presentation of the company, etc.

It is very useful, and finally, I can say:

While requesting the estimation, customers look for the details about the future cooperation. duration, engagement model, and the final cost. Create a complete business proposal. It may include negotiations results, technical solution, and information about your company. Highlighting the main points shows your care and respect.

See What Matters

understand clients: see what matters

I became a client for a while. I had a chance to make sure of a great importance of knowing your customer. I checked cooperation systems implemented at WEB4PRO and the other companies first-hand. Now I know what the clients want.

Qualitative portfolio. The more complicated, beautiful, and modern works you show, the better.

Video Skype call. There is more trust between people who can look at each other. If we take WEB4PRO, here we always talk via Skype using video if the client turns on the camera.

Understanding the problem. It’s nice to get some questions about your business, goals, target audience, differences from competitors. That shows you know how to meet customer expectations.

Lean business processes. It’s very useful when the company knows how to work with clients. It’s great when their managers are always ready to help you out. That makes you feel convinced.

Open hourly rates. Good when executors don’t hide their hourly rates. A clear estimation should include the cost for project managers, analytics, QAs works.

Detailed Estimation. It’s better to express the project duration in hours. Also, breaking the process into milestones makes the difference. That helps the client understand the final costs.

Friendly communication. When you feel convenient speaking with a person, you’ll like to talk again. You event don’t pay too much attention to high rates and prices if the person is friendly.

Adherence to deadlines. If the executor sticks to the schedule at the beginning, they might adhere to the deadlines along the way.

Long-term partnership. I noted the company which described their partnership with Australian clients. Their projects were large enough. Who knows, but they tried to support their achievements. But all in all, they impressed me.

Results of negotiations. It’s pretty useful to receive an email with the results of negotiations after the meeting. This type of report is a must-do point at our company. So, now I’m confident in its efficiency and necessity.

Business proposal. It helps to sum up the main ideas and points of cooperation. A good marketing kit provides the full picture of the business processes you are going to follow.

What you can do to be on the same wavelength with a customer:

  • learn client’s project requirements;
  • wonder about the client’s values and business ideas;
  • don’t ask the needless questions: try to find the answers in the project specification;
  • don’t hide your hourly rate. Be honest;
  • prepare detailed project estimation;
  • and the main: keep your word.

Do we have anything to improve? Of course! Anyone who works with customers should always work on understanding clients needs. But it’s pleasant to note that we are on the right way at WEB4PRO. Our work processes are well-thought-out. They help the whole team work well and cooperate with customers with the high efficiency.

What do the clients want? Well, the clients want to understand what happens and what they pay for. They want to be confident in the executor, and they want to secure themselves from risks. Finally, a client is a person, who wants to deal with friendly, honest, and professional people.

How to understand clients? – Step into their shoes at least once. Then a lot of steps and routine work points will matter much more to you. These are my words, the words of Project Manager:)

P.S. This rule works with any person, not only with your clients. As Harper Lee says in the book To Kill A Mockingbird, “You never understand a person… until you climb into his skin and walk around in it.”

The featured image: the scene from “What women want”. The main character climbed into women skin in the truest sense of the word.

Migration from Magento 1 to Magento 2 using Magento 2 Migration Tool

Welcome! Today we’ll show you how to migrate Magento® 1 to Magento 2 using Magento 2 data migration tool. You’ll also learn how you can manage themes, extensions, and customization. Together we’ll follow the next script:

Let’s get the ball rolling!

Magento 2 Usage Statistics

Magento 1 stops official updates in November 2018. According to BuiltWith, the number of live websites using Magento 2 grew up to 13500 at the moment (July 2017). There were 11000 live websites in May 2017. As you can see, Magento 2 becomes more and more popular, while the first version still exists. If you’ve made a decision to move to Magento 2, there is a complete guide on how to do this. But this task requires strong web development skills, some effort and a bit of patience:)

Magento 1 to Magento 2 Migration Process

While migrating your website from Magento 1 to Magento 2, you’ll work with four components:

  • data;
  • themes,
  • extensions and custom code;
  • customization.

Each of these components requires a certain approach.

Our today’s task is to migrate data with Magento 2 Data Migration Tool. Follow our step-by-step guide below, and you’ll make it!

Migrating Data with Magento 2 Data Migration Tool

Before we start, we should make several preparation steps. Let’s run through them.

Step 1: Software and Hardware Requirements

Preconditions

  • Check system requirements. Check parameter in php.ini

memory_limit: more than 512M

  • Install Data Migration Tool. Versions of Magento and Data Migration Tool must match.

How to Install Data Migration Tool

First, you need to edit composer.json in the Magento root directory. This way you provide the location of the Data Migration Tool package: “URL”: “https://repo.magento.com/.” Also add to require “magento/data-migration-tool”: “version”, where “version” must match the Magento 2 code base version.

  • Create a Magento 2 database backup. This will allow you to restore the initial database state if migration is not successful.
  • Check the network access to connect Magento 1 and Magento 2 databases.
  • Copy Magento 1.x media files to Magento 2.x. You need to copy them manually from magento1-root/media to magento2-root/pub/media directory.
  • Stop all Magento 1.x cron operations.
  • Stop making any changes in Magento 1.x Admin except for order management and in Magento 2 Admin and storefront.
  • Remove outdated and redundant data from Magento 1.x database (logs, order quotes, recently viewed or compared products, visitors, event-specific categories, promotional rules, etc.).

Step 2: Theme Migration

As for themes, we’ll need to make changes to themes and customizations for Magento 2, because the system’s hierarchy is completely different from Magento 1. So, you’ll find lots of space for your creativity and innovation.

Step 3: Extension Migration

We can go to Magento Marketplace or MagentoConnect and get new Magento 2 extensions with their latest versions. Also, we can develop a new custom solution. If you want to migrate the custom code, you can use Code Migration Toolkit presented by Magento on Github.

According to the Magento community, migration to Magento 2 is 20% more complicated than updating Magento to the latest minor version. Opinions differ. It depends on the level of the developer’s experience and the project complexity.

Step 4: Data Migration

Magento 2 Data Migration Tool is a ready-made assistant for migrating the website data. We can move all our customers, products, store configurations, order and promotions data to Magento 2 with its help. It is the process we’ll focus on in our article.

Configuring Migration to Magento 2

If we need to migrate Magento 1.x EE to Magento 2.x EE, the mapping and configuration files will be located in the following directory:

<your Magento 2 install dir>/vendor/magento/data-migration-tool/etc/ee-to-ee

Create a config.xml from the provided sample config.xml .dist.

If we migrate data from Magento 1.14.1.0, the config.xml and map.xml. will be located in the following directory:

<your Magento 2 install dir>/vendor/magento/data-migration-tool/etc/ee-to-ee/1.14.1.0

But before, enable to perform custom database mapping between your Magento 1 and Magento 2 databases.

Specify the access to the databases in config.xml:

<source>
    <database host="127.0.0.1" name="magento1" user="root"/>
</source>
<destination>
    <database host="127.0.0.1" name="magento2" user="root"/>
</destination>
<options>
    <crypt_key />
</options>

The <crypt_key> tag must be filled in. You can find it in the local.xml file. The file is located in Magento 1 instance directory at app/etc/local.xml in the <key> tag.

Migration to Magento 2 Modes

Overall, migration from Magento 1 to Magento 2 consists of three phases (modes):

  • Settings: migrates configuration settings.
  • Data: bulk migrates the main data to the database.
  • Delta: transfers incremental data updates added to Magento 1 storefront and Admin Panel while running previous migration modes.

Each mode is declared in config.xml and divided into steps. Each step is responsible for transferring particular data. At the begin of the run, the step checks Magento 1 and Magento 2 table structures for consistency. Then the actual data is transferred to Magento 2. In the end, this data is verified.

If you don’t want to migrate some data from Magento 1 to Magento 2, you just need to disable or remove the specific step in config.xml. For example, remove the following step from Data mode:

<step title="Log Step">
   <integrity>Migration\Step\Log\Integrity</integrity>
   <data>Migration\Step\Log\Data</data>
   <volume>Migration\Step\Log\Volume</volume>
</step>

And remove this one from delta mode:

<step title="Log Step">
   <delta>Migration\Step\Log\Delta</delta>
   <volume>Migration\Step\Log\Volume</volume>
</step>

Furthermore, if we do not want to track the changes in the log_visitor table, we need to remove “delta_log” group in deltalog.xml.dist file:

<group name="delta_log">
   <document key="visitor_id">log_visitor</document>
</group>

Migrating Settings and Data to Magento 2

So, we successfully moved to the finish.

To start migrating settings, run:

bin/magento migrate:settings [-r|--reset][-vvv] {<path to config.xml>}

where:

  • [-r|–reset] is an optional argument that starts the migration from the beginning. You can use this argument for testing migration.
  • {<path to config.xml>} is the absolute file system path to config.xml.
  • [ -vvv] – you can use this argument to output more verbose messages in the console.
<your Magento 2 install dir>/vendor/magento/data-migration-tool/etc/ee-to-ee/1.14.1.0/config.xml

To start migrating data, run:

bin/magento migrate:data [-r|--reset] {<path to config.xml>}

To start migrating incremental changes, run:

bin/magento migrate:delta [-r|--reset] {<path to config.xml>}

Incremental migration enables to migrate only data that customers added via storefront (created orders, reviews, changes in customer profiles, etc.) and all operations with orders in Magento Admin panel.

Incremental migration runs continuously until you stop it by pressing CTRL C.

Reindex all Magento 2.x indexers after migration.

Possible Problems

We can get the following error during Data Integrity Step:

  • Error: Foreign key (<KEY_NAME>) constraint fails. Orphan records id:<id> from <child_table>.<field_id> has no referenced records in <parent_table>

Solution: There are missing database records in the parent_table. The field_id of the child_table is pointing to this parent_table.

If we can not delete the records from the child_table, we can just disable the Data Integrity Step in config.xml:

<step title="Data Integrity Step">
<integrity>Migration\Step\DataIntegrity\Integrity</integrity>
</step>

The data migration tool recognizes the differences in database structure between Magento 1.x versions. Most of these database structural differences are declared in map files. Each step in the process uses map files to transform data for use in the Magento 2 store. When differences are not declared in map files, then the Data Migration Tool displays an error and does not run.

If some Magento 1 entities (in most cases, coming from extensions) do not exist in Magento 2 database, the following errors may occur:

  • Error: Source documents are not mapped: <EXTENSION_TABLE>

Solution: Install the corresponding Magento 2 extensions or ignore the problematic data in map.xml (or map.xml.dist).

<source> 
  <document_rules> 
      <ignore>
          <document><EXTENSION_TABLE</document>
      </ignore> 
  </document_rules> 
</source>
  • Error: Source fields are not mapped. Document: <EXTENSION_TABLE>. Fields: <EXTENSION_FIELD>

Solution:

<source> 
  <document_rules> 
      <ignore>
          <document><EXTENSION_TABLE>.<EXTENSION_FIELD</document>
      </ignore>
  </document_rules> 
       </source>
  • Error: Destination documents are not mapped: <EXTENSION_TABLE>

Solution:

<destination>
<field_rules>
<ignore>
     <field><EXTENSION_TABLE>.<EXTENSION_FIELD></field>
</ignore>
</field_rules>
 </destination>
  • Error: Mysql server has gone away

Solution: Increase the following value in the /etc/mysql/my.cnfmysql configuration file:

  • max_allowed_packet = 2000M
  • wait_timeout = 6000

3 Best Magento SEO Extensions in 2019

We continue to work on Magento® SEO. Here you’ll find Top 3 Best External Magento SEO extensions for performing the best search engine optimization practices. We have already learned the things that matter in Magento SEO and how to set them up.  Now there is an additional help for making your Magento website SEO-friendly.

Best External Magento SEO Extensions: Our Choice

All steps to SEO become much easier and convenient with external Magento SEO extensions. We‘ve chosen three of them which are the best in our humble opinion. Here they are:

SEO Toolkit by Amasty

This extension includes the numerous features of Magento SEO. It will help you to do the following:

  • work with categories and products;
  • create relevant metadata for all pages;
  • export and import product tags;
  • generate the sitemaps for a multiple Magento online store;
  • create SEO-friendly short URLs;
  • make SEO-friendly product reviews.

It’s a really good solution, and it comes with a user-friendly guide on how to make all thing works.

Where to find the extension and guide: amasty.com

There you’ll also find the detailed description of a product.

Magento SEO Suite Ultimate extension v9.0.1 by Mageworx

This is the all-in-one Magento SEO solution. It can do the same things as the previous one. So you can choose the extension you like more. Let’s take a look at what it can:

  • has 20 templates for work with metadata;
  • helps to create advanced Magento snippets;
  • performs SEO for category and product pages;
  • includes features for SEO breadcrumbs and redirects;
  • allows external and internal cross-linking;
  • generates extended XML and HTML sitemap;
  • provides the detailed SEO reports.

It is also packaged with users guide that will help you to achieve the best results in working with Magento SEO features.

Where to find the extension and guide: mageworx.com

Create HTML Sitemap for SEO by Creare – Free

The extension is focused on creating an HTML sitemap for Magento store, and it’s available for free on MagentoConnect. Here is how it works::

  • lists all Magento store categories in cascading style;
  • creates the SEO-friendly sitemap;
  • replaces the defaulted paginated sitemap of Magento;
  • lists all static and CMS pages;
  • helps to make all category pages indexed;
  • has built-in XML checker.

Where to find the extension: magentocommerce.com

Ultimate Magento SEO Guide: What Matters and How to Setup

Anyone who runs Magento® online store aims to reach its popularity, get more customers, and as a result, more sales. So what factors affect the success of the online store? Definitely, they are the high quality of products and services, meeting the market expectations, website performance which influences conversion rate, and SEO.

Magento SEO is similar to the optimization process of any other website. But on the other hand, Magento online shop is much more complicated. It has more pages, categories, products, and features. So what exactly matters in Magento SEO and how to set up everything correctly? Today we’ll make it clear. Meet our ultimate guide which contains certain steps on how to make Magento online store SEO-friendly and popular. Our Magento SEO technical recommendations will help you not forget all these important things.

Magento SEO Content Structure

When we say “structure”, it means the whole content logic in HTML of each web page. In Magento, there are different types of pages, such as home page, contact page, category page, product page, and more others. And by the way, the last ones are the most multiple types. Each page must have a certain content structure to be SEO-friendly. We are going to consider the main structural elements in the context of Magento.

When Google’s bot goes through the web, it scans the web pages using the “links” it can base on, like titles, meta keywords, meta description, alt-texts of images, the logically built headers, and URLs. It aims to provide the users with the most relevant information, and these elements together help it learn the level of relevancy. It goes through the sitemap and stops on the pages that are marked like robot.txt (the pages which are not enabled to index). So this structure must be logically clear and give the certain sense of the content you post. If it is, each web page gets indexed and becomes visible for web users.

You can carry out and control your SEO using Google Search Console.

Magento SEO Titles

The title is the heading of your web page. It is the first visible element in Google search results colored in blue. It’s quite important to create a unique title for each Magento page (category, product, home and others.). And the main: a title should contain your focus keywords.

It looks like: Keyword1 Keyword 2…- Your Company Name

Setup titles in Magento: System -> Configuration -> Design-> HTML Head-> Default Title.

Example: In case the category page for business laptops, use a title like:

Business Laptops – Computer Shop. (“Computer Shop” is imagined brand name).

Magento SEO Metadata

Metadata is one of the most important types of information for search engines. It contains keywords, meta description. We’ll not stop on how to make a keyword research or write a description. You can read more about SEO-elements if you need.

You should just know that it’s necessary to pick the focus keywords for each page.

Setup keywords in Magento: System -> Configuration -> Design-> HTML Head-> Default Keywords.

Setup description in Magento: System -> Configuration -> Design-> HTML Head-> Default Description.

Magento SEO URLs

Here we’ll look at the common issues related to URLs you should keep in mind.

Human- and SEO-Friendly URLs

Don’t forget to include the keywords and relevant information to your URLs, where your pages sit. Each URL should be readable and tell the relevant information. Let’s take a look at some product page. Here is an example of good URL for the certain product: yourdomainname.com/puma-sneakers-for-run-white

Avoiding Duplicate Content

The duplication of content happens in several cases. We’ll consider them below

Domain Name Issue

The first one happens when your shop is delivered by the both types of domain name: with www and without www. It looks something like this:

http://www.websitename.com

http://website.com

If your Magento store is loaded with any of this types, congratulations, you have the duplicate content. But you can fix this and set up your domain name in Magento: System –> Configuration –> Web –> Unsecure –> Base URL.

Enter and save your domain name. Then make sure that your website doesn’t generate duplicate content and is available only by the domain name you’ve chosen.

Enable this option (choose „Yes“) using the following way: System –> Configuration –> Web –> Search Engines Optimization –> Use Web Server Rewrites.

Rel=Canonical Turning On

Turn on rel=canonical for the categories and products. It’s pretty helpful in reducing the number of duplicate content.

Setup rel=canonical: System –> Configuration –> Catalog –> Search Engine Optimisation -> „Use Canonical Link Meta Tag For Products“ and „Use Canonical Link Meta Tag For Categories“.

Set Up Redirects

Here the function Create Permanent Redirect for old URLs if Url key change will be helpful when you migrate your website or apply the certain changes to your URLs. Choose “Yes” for this function.

How to turn it on: System –> Configuration –> Catalog –> Create Permanent Redirect for old URLs if Url key changed

Don’t Set Up Categories Path for Product URLs

It’s better to turn off this function and choose “No” for it, in order not to make Google confused when you change the category name and all products set on this path can vanish from the index.

How to turn it off: System–>Configuration–>Catalog–>Use Categories Path for Product URLs

Also, there are some useful extensions that can help you on your way to great SEO.

Magento SEO Headings

Headings divide the page content into parts and name them. In HTML, headings follow locally one by one like H1, H2, H3, H4 tags and so on so forth. You should follow this logic and not miss the heading level when creating your content. H3 should follow H2. You can’t miss H4 and place H5 after H3. Also, include your keywords here.

Magento SEO Image Attributes

Everyone who carries out SEO should know that the search engines are not able to see what in the picture is. The only way to help them is to add the image attributes: image title and alt-text.

If talk about Magento with its numerous product images, this moment is quite critical here. Alt-text and image title should also include the keywords.

Magento SEO Links

Such important content elements, as links and anchors, should also contain the keywords. Internal site links for SEO are under your control in Magento, but also the system generates the category-related links automatically. Each page should be reachable through the website. For these purposes, webmasters should create a sitemap.

Sitemap.xml and Robot.txt in Magento SEO

These elements are devoted to helping the search engines follow the right way through the website. Sitemap.xml is a listing data of your store pages and links.

Where to find the Sitemap: Admin Panel -> System -> Configuration -> Google Sitemap. Then Enable it in Catalogue -> Sitemap.

A tip: it’s better to do this process after your website release. Before, don’t turn on “index, follow”, and use subdomain with an access password in the stage of development.

Setup Index Follow: System –> Configuration –> Design –> HTML Head –>  Default robots “INDEX, FOLLOW”. – Turn on after release.

Check your sitemap file. Only then add it to Google Webmasters Tools.

If you run a multiple Magento online store, for example, Chinese and German, separate your sitemaps and call them like sitemap-ch.xml and sitemap-ge.xml.

Robot.txt is the file that contains the information about the website URLs Google’s robot should not crawl through. But note that it could keep the pages in the index (but not show the other elements, like title and description for these pages). Creating this file is a process that needs to be well-thought-out and performed. You can read more about it in Inchoo guide.

Content Quality in Magento SEO

The content quality definitely matters in SEO. What does it mean?

First of all, it’s the relevance of the content. Google’s most task is to provide the users with the most relevant information.

If you pick the keywords, they should make sense in the website texts, such as category and product descriptions, product cards, landing pages, and so on so forth.

The next thing is that the content should be unique, interesting, and attractive. Never try to use someone’s content or copy it from the other website. Such actions are forbidden by Google. Don’t only advertise. Give something useful.

Never use the black methods like hidden, bought, and broken links, spam, links in a bad neighborhood, overlinking. They will not boost the brand but destroy the reputation.

And remember the following: the more traffic generates your online store, the more conversion it gets, and the more popular is your brand, the higher position in the search results it gets.

Measurement of Magento SEO Results

It is not a step. It’s a continuous process that we have to for performing the best SEO practices. You should know how to measure results from the SEO campaigns. And here Google’s tools are very useful. The best helpers for watching your SEO are Google Search Console, and the most powerful platform for deep analysis of traffic sources is Google Analytics. Try to make friends with Google, and your Magento SEO will be much better.

Full Page Cache Magento Extensions for the High Website Performance

Last time we discussed how to optimize Magento® performance. It’s a pretty important process for everyone who owns the website, especially eCommerce online store. Page loading time affects the conversion rate dramatically. It’s an interesting fact that if the page is loaded more than 3 seconds the visitor goes away. In the worst scenario, a website is hopeless at selling products, and you lose potential customers. But everyone can avoid it and turn this task into benefits. By the way, the search engines consider page loading time. And the websites that load faster get the higher positions in search results.

If your website is based on Magento, you are familiarized with its power. And that means your online store resists a high load of visitors on a daily basis. Is it ready for such a huge pressure, for example, when you run season sales at your store, and the number of visitors goes sky high? You can check your Magento based website for the most important parameters using our checklist. If you find something you can improve, just do it.

Checklist on How to Speed up Magento

  • Migrate to the Latest Version
  • Perform Image Optimization
  • Enable Flat Catalog
  • Concatenate (merge) CSS and Javascript Files
  • Enable Magento Compilation
  • Utilize Caching
  • Choose Fast Web Hosting
  • Clean Up Magento Database & Logs
  • Download Fewer Extensions & Modules
  • Perform MySQL Configuration
  • Update Indexes
  • Use PHP Accelerators

Now you can move on to the list of Magento Extensions to improve your website performance with full page caching.

Classic Scheme of Full Page Cache Magento Extensions

Imagine the user. He is opening the page of some Magento online store the first time. What happens then? Here are where Magento starts its work. It creates and delivers the required web page to a user and saves its copy in cache. This copy is stored there, and next time when the visitor goes to this page, he gets this copy. So, you see that the way is shortened. It goes from the user to cache. This process increases page loading speed in many times.

Top 3 Useful Full Page Cache Magento Extensions

As we work on Magento on a daily basis, and Magento development is our main expertise, we face different tasks related to Magento performance optimization. We can give you several recommendations on the choice of the useful full page cache Magento extensions based on our technical experience. You can try them and speed up your Magento based website. Before, you can view our checklist to learn what items you can improve right now.

Full Page Cache by Mirasvit

This solution works following the classic scheme of page caching and helps to increase the speed of Magento based website. It returns the web pages copies from the cache immediately. It allows you to reduce page loading time in 6-25 times comparing to default Magento. And this is an advantage of the extension.

Features:

  • full page cache for all website pages;
  • excludes required pages from the cache;
  • supports themes for mobile devices;
  • you can use different types of cache;
  • built-in crawler.

It also includes the detailed crawling statistic that helps you to keep cache up-to-date.

Where to get and learn more: https://mirasvit.com/

Full Page Cache by Amasty

Full Page Cache by Amasty also helps you to decrease server load and speed up your website. The distinctive feature of this extension is the support of dynamic blocks, called “holes” punch. So you can choose whether you want to use dynamic block caching or not.

Features:

  • full page cache for web pages;
  • dynamic blocks caching (enable/disable);
  • supports themes for mobile devices;
  • clearing cache for the whole website and its specific pages individually;
  • caching of Ajax requests;
  • uses native Magento functionality and compatible with default Magento;
  • Includes full page cache crawler;
  • supports LSS websites and compatible with content delivery networks.

Where to get and learn more: amasty.com

Lesti_Fpc Extension

Lesti_Fpc extension is an internal solution for Magento. As external extension work in front of Magento, they reload dynamic content with Ajax. Yes, they are fast and efficient. But if you need more flexibility, Lesti_Fpc extension is one of the best solutions for you. It is an internal extension. It requires a bit more time for caching but could be compatible mostly with any Magento theme without much customization and development processes.

And a great plus that it’s absolutely free.

Features:

  • full page caching;
  • works with events;
  • replaces dynamic fields;
  • can exclude a category and a product;
  • 24-hour cache lifetime;
  • compatible with 1.5 – 1.9 Magento CE versions.

Where to get and read the instruction: github.com

Where to learn more: gordonlesti.com

How to Optimize Magento Performance Without Coding

If you have a website based on Magento®, sooner or later, you face the question of how to optimize Magento performance. We have already discussed how page loading speed influences conversion. So it’s time to make it clear and finally get the working steps on how to speed up Magento based website. Let’s start.

Magento Performance and Website Speed Test

Although the process of Magento performance optimization is quite difficult and depends on different parameters, there are several basic things that can help you decrease your website loading time. You can speed up Magento by yourself using our Magento optimization techniques. Let’s start from scratch and check whether your website is optimized or not. Then run the Google speed test and learn how fast your website is.

If you see that your page loading time needs to be reduced, we can move on.

Magento Performance Tuning

The process of performance optimization for any CMS and website is quite similar: we should keep up with the times and reduce page loading time by reducing the weight of content and code. We need to shorten the way of servers request. How to do it? Let’s do the following:

Use the Latest Version of Magento for Better Performance

It’s quite important to use the latest version of CMS and modules because the bugs are fixed, and performance is improved in updates. Also, it will help you to keep your Magento based website protected.

Optimise Images and Reduce Their Weight

Surprisingly, but more than 50% of page loading time is taken by images loading. Here we move to reduce the weight of content. So, where are the images on your website? For sure, you’ll find them in Products and Categories. If you just decrease their size and use the compression, it will drastically affect the page loading time. You can do it with help of different extensions, but also the simple way is to use “Save for Web” in Adobe PhotoShop (you’ll find this function in header menu, “File” tab).

Flatten Your Categories and Products

Imagine that your website has a huge number of categories and products. And all of them have their own attributes. These attributes are stored in separate databases. So while reaching some product from some category, the way of the request is too long. That’s why if you enable flat categories and products, the attributes will be in one table. Therefore, you shorten this way. Magento website speed will be increased.

How to enable flat categories and products:

  • go to Magento admin panel;
  • choose System in top menu;
  • then choose Catalog.
  • Catalog Frontend and set Yes for “Use flat Categories” and “Use flat products”.

Combine CSS and JS Files into One File

This will also have a positive impact on a website performance. It’s useful when it comes to the time of server request. If you merge CSS and JS files, you’ll shorten the way of request and optimize your code.

How to merge CSS and JS file:

  • go to Magento admin panel;
  • choose System in top menu;
  • then choose Configuration, Advanced.
  • go to Developer, CSS settings, and JavaScript settings;
  • there you should set Yes for “Merge Javascript” Files and “Merge CSS” Files.

We are on the right way. We have already done the most important steps. Let’s move to the next one.

Enable Compilation and Activate the Scripts

This step helps to activate some important scripts in Magento core, and the website becomes faster.

How to enable compilation:

  • go to Magento admin panel;
  • choose System in top menu;
  • then choose Tools, Compilation, Enable.

Enable Caching

Cache is quite a powerful element when it comes to website performance optimization. It helps to shorten the way of server request by storing different data that was loaded before. There are different types of caching in Magento. We offer you to enable all of them and look at the result.

How to enable caching:

  • go to Magento admin panel;
  • choose System in top menu;
  • then choose Cache Management, select all items and choose Enable.

Magento admin panel

Also, don’t forget to turn on the page cache. It’s a pretty useful and powerful way to increase your Magento website speed. You can use special extensions for these purposes, for example:

Zoom Full-Page Cache, LiteMage Cache, Product & Category Page Cache

Choose the Fastest Web Hosting

It’s an important step which you should do right at the beginning of your Magento performance optimization. It is a base for your website, that’s why it should be fast and reliable enough in order to resist the high load and perform the best efficiency. It’s better to prefer not cheap but well known hosting providers with a good reputation and reliable technical support.

How to Install Magento 2 on Windows 10 Using Local OpenServer

Install Magento® 2 on Windows 10… Sounds good, right? Today we’ll run through the process on how to make it work using OpenServer.

Magento Installation Steps

Anytime you are going to deal with Magento 2, you’ll find the list of specific technical requirements, such as the following ones:

Magento 2 technical requirements

It’s just a part of them. The first requirement is operating system requirements: Linux x86-64. But should we do if our task is working with Magento 2 on Windows 10? OpenServer is a right solution in this case. The matter is that symlinks don’t work on Windows 10 for Magento 2.

However, OpenServer and the correct install of Node.js are helpful in solving this problem. It’s our way of how to work with Magento 2 and Node.js. Enough words – follow our Magento install instructions guide.

Setting the local OpenServer in order to Install Magento 2 on Windows 10

  • Install Node.js if it’s not set. You can find the Node.js official installer here. It is set to Program Filesnodejs by default.
  • Run OpenServer with admin access. The symlinks will work only in case you use admin access.
  • Open settings. On the ‘Server’ tab and “Setting the use of Path variable” field, we should set ‘Own Path userdata/config/path.txt’.
  • Following the path: Your_drive:OpenServeruserdataconfig (Your_Drive is a drive’s letter where OpenServer is set), create a path.txt file with the next content:

C:Program Filesnodejs

C:UsersyouAppDataRoamingnpm

The second row here is a path of setting grunt, gulp, etc. If you have the other files location, don’t forget to correct the paths.

  • Rerun the server.
  • Run OpenServer console.
  • Check whether node.js works. Use node v command.
  • Install Grunt: npm install -g grunt-cl
  • Check whether Grunt works. Use grunt v command.

Now Node.js and Grunt are set on your OpenServer globally.  You can set any other packages, such as Bower, Gulp, etc using our approach.

The main thing here is to write path.txt. file paths correctly. That’s all!

Magento Import Tax Rates. The Full Guide

Magento® 2 provides the calculation of the tax amount when creating an order. Magento firstly was developed for the USA, because sales tax rates can be different in different locations (county/city). The default Magento 2 functionality allows setting the tax rate by the following parameters: country, region (state/province), postcode. The administrator can set the tax rates in Stores -> Tax Zones, and Rates menu on admin panel. But as the USA has a lot of locations with different postcodes, it could be a bit challenging to output all necessary tax rates pegged to the postcode manually.

Now we’ll consider the way of how to do this.

How In Magento Set Tax Rate Programmatically

Let’s imagine the CSV-file of Magento tax class for shipping rates (more than 1000 rows) for Northern Carolina state that has the following format:

State,ZipCode,TaxRegionName,StateRate,EstimatedCombinedRate,EstimatedCountyRate,EstimatedCityRate,EstimatedSpecialRate,RiskLevel

We’ll pay attention to a ZipCode field which contains the location postcode and Estimated Combined Rate presented in the decimal format.

$api_url = "….";//link to Magento 2 Store
$ch = curl_init();
curl_setopt($ch,CURLOPT_POST,true);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch,CURLOPT_POSTFIELDS,json_encode(array('username'=>'admin','password'=>'...')));
curl_setopt($ch,CURLOPT_HTTPHEADER,array("Content-Type:application/json"));
curl_setopt($ch,CURLOPT_URL,$api_url.'rest/V1/integration/admin/token/');
$res = curl_exec($ch);
$result = json_decode($res,true);
if(isset($result['message'])){
  echo $result['message'];
  exit(0);
}
$token = $result;//Getting the connection token 
$file = fopen('TAXRATES_ZIP5_NC201701.csv','rt');
$header = fgetcsv($file);
while($row = fgetcsv($file)){
   $taxArray = array(
     'tax_country_id'=>'US',
     'tax_region_id'=>44,
     'tax_postcode'=>$row[1],
     'code'=>'US-NC-'.$row[1],
     'rate'=>$row[4]*100
   );
    curl_setopt($ch,CURLOPT_POST,true);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch,CURLOPT_POSTFIELDS,json_encode(array('taxRate'=>$taxArray)));
curl_setopt($ch,CURLOPT_HTTPHEADER,array("Content-type: application/json","Authorization: Bearer ".$token));
curl_setopt($ch,CURLOPT_URL,$api_url.'rest/V1/taxRates');
$res = curl_exec($ch);
echo $res."<br/>";   
}
fclose($file);

This example describes the tax rate to percent value because the tax rates are set exactly in this format in Magento 2. As we needed to place the configuration of the tax rates only for Northern Carolina, it was not difficult to find the identifier of this state in Magento based online store. We created an entry in tax_calculation_rate table for each location by requesting _/V1/taxRates() API method and transmitting it the array in JSON format as a parameter.

If the file provided the data for locations in different states, we would better get the array of states using_/V1/directory/countries/US() API method which would return the array with information about the USA from Magento 2. This array would include state id – state code binding. First, we would get the state code, and then we would place the state id in the request for the appropriate state code from State column.

Magento 2: How to Add a Custom Attribute to The Customer’s Address

When developing and modifying an online store, it often comes up the task to add attributes to the entities. Let’s suppose; there is an online store based on Magento® 2 which retails the products not only for customers but also for some businesses (corporate clients). The process requires that the on the client must indicate whether the shipping address is the Residence or the Business on the order page. The data entered by the customer must get to the order address. In case the customer is authorized, these data must also get to the client’s address book for later use.

When having this task, you may face two difficulties:

  • correct implementation of the entered data storage (backend);
  • displaying the attribute input (select) field on the checkout page (frontend).

We’ll consider these tasks below.

Magento 2: Adding a New Field In Address Form

In Magento 2, the checkout page is formed using the knockout javascript component. This component gets the fields which the user can output from the configuration. The configuration is transferred from the server to the browser in JSON format. The output of system fields is implemented in core, but the output of an additional field must be implemented with the extension. You can look through the example of adding the field here.

Magento 2: Implementation of The Entered Data Storage

Now we’ll pay close attention to the backend, and, more exactly, implementation of the entered data storage. The most required for the way of data storage is the easy access to data and the ability to copy them.

In Magento 2, the customer’s address from the address book is EAV. Therefore, it can have additional attributes which are stored in tables created by Magento developers. You can easily add the attribute using the installation script of InstallData module, for example, using the following method:

$eavSetup = $this->_eavSetupFactory->create(['setup' => $setup]);
        $eavSetup->addAttribute('customer_address', 'type', array(
            'type' => 'int',
            'input' => 'select',
            'label' => 'Address Type',
            'source'=>'Web4pro\Ajaxcart\Model\Address\Type',
            'global' => 1,
            'visible' => 1,
            'required' => 1,
            'user_defined' => 1,
            'system'=>0,
            'group'=>'General',
            'visible_on_front' => 1,
        ));
        $eavSetup->getEavConfig()->getAttribute('customer_address','type')
               ->setUsedInForms(array('adminhtml_customer_address','customer_address_edit','customer_register_address'))
               ->save();

An additional attribute will be created, and its values will be stored in customer_address_entity_int table because the attribute is not static. The static attributes are stored in customer_address_entity table, and all system attributes of this entity are static.

When the user adds the product to the shopping cart, goes to the checkout page, and enters their address, the address is saved not to the address book (there could be no address book if the user is not authorized), but to the shopping cart entity. Physically, it’s quote_address table, and we can work with it with \Magento\Quote\Model\Quote\Address. The user can enter two addresses overall: a payment address and a shipping address. When creating the order, addresses from the shopping cart are copied to the order (sales_order_address table and \Magento\Sales\Model\Order\Address). These models are not EAVs. Mainly, Magento 2 implementation is similar to Magento 1 one, but at the same time, there are several important differences.

Magento 2: Working with Address Attributes

In Magento 2 and Magento 1, the task to copy address fields is implemented with a fieldset configuration. In Magento 2, etc/fieldset.xml file in module configuration is responsible for this. In Magento 1, we would add the necessary fields to quote_address and sales_order_address tables, write the attributes to the fieldset configuration, and the core code would have copied the necessary values. But in Magento 2, it doesn’t work. And this is why.

The data copying is implemented in Magento\Framework\Api\DataObjectHelper class in public populateWithArray() method which calls protected() method with practically the same parameters. Look at its implementation below:

protected function _setDataValues($dataObject, array $data, $interfaceName)
    {
        $dataObjectMethods = get_class_methods(get_class($dataObject));
        foreach ($data as $key => $value) {
            /* First, verify is there any setter for the key on the Service Data Object */
            $camelCaseKey = \Magento\Framework\Api\SimpleDataObjectConverter::snakeCaseToUpperCamelCase($key);
            $possibleMethods = [
                'set' . $camelCaseKey,
                'setIs' . $camelCaseKey,
            ];
            if ($key === CustomAttributesDataInterface::CUSTOM_ATTRIBUTES
                && ($dataObject instanceof ExtensibleDataInterface)
                && is_array($data[$key])
                && !empty($data[$key])
            ) {
                foreach ($data[$key] as $customAttribute) {
                    $dataObject->setCustomAttribute(
                        $customAttribute[AttributeInterface::ATTRIBUTE_CODE],
                        $customAttribute[AttributeInterface::VALUE]
                    );
                }
            } elseif ($methodNames = array_intersect($possibleMethods, $dataObjectMethods)) {
                $methodName = array_values($methodNames)[0];
                if (!is_array($value)) {
                    if ($methodName === 'setExtensionAttributes' && $value === null) {
                        // Cannot pass a null value to a method with a typed parameter
                    } else {
                        $dataObject->$methodName($value);
                    }
                } else {
                    $getterMethodName = 'get' . $camelCaseKey;
                    $this->setComplexValue($dataObject, $getterMethodName, $methodName, $value, $interfaceName);
                }
            } elseif ($dataObject instanceof CustomAttributesDataInterface) {
                $dataObject->setCustomAttribute($key, $value);
            }
        }

        return $this;
    }

As you can see, this method searches through the incoming array of data and writes them into $dataObject. Only determined methods in the object class are used for writing. If the writing method isn’t found and the entity implements CustomAttributesDataInterface, it will try to write a value with setCustomAttribute.

\Magento\Quote\Model\Quote\Address and \Magento\Sales\Model\Order\Address models implement this interface, but we will not be able to use it for the following reasons. This method checks the presence of enabled attributes which _getCustomAttributesCodes() method returns only when our key matches the attribute code. _getCustomAttributesCodes() method is implemented in Magento\Framework\Model\AbstractExtensibleModel class which is a parent for \Magento\Quote\Model\Quote\Address and \Magento\Sales\Model\Order\Address. But this method returns the empty array and it is protected, so we can’t add our own attribute codes using the plugin. We can only redefine the class with dependency-injection which is an undesirable way of customization.

That’s why we’ll use another way. All classes which are inherited from Magento\Framework\Model\AbstractExtensibleModel also support extension_attributes. The attribute of this kind should be declared in etc/extension_attributes.xml file of the module. The attribute could have a simple scalar type (int, string) as well, as a type of some class. A module developer can implement the saving of this attribute, but Magento also has the standard built-in tools of attributes data uploading. We can’t save this attribute into the table where the main entity which it’s added to is placed. It should be a separate table, and we can put the link to this table in the description. In order to do this task, we’ll show the content of etc/extension attributes.xml file below.

<?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\Quote\Api\Data\AddressInterface">
        <attribute code="type" type="int">
            <join reference_table="web4pro_quote_address" join_on_field="address_id" reference_field="address_id">
                <field column="type">type</field>
            </join>
        </attribute>
    </extension_attributes>
    <extension_attributes for="Magento\Sales\Api\Data\OrderAddressInterface">
        <attribute code="type" type="int">
            <join reference_table="web4pro_order_address" join_on_field="entity_id" reference_field="entity_id">
                <field column="type">type</field>
            </join>
        </attribute>
    </extension_attributes>
    <extension_attributes for="Magento\Customer\Api\Data\AddressInterface">
        <attribute code="type" type="int"/>
    </extension_attributes>
</config>

The extension attribute is defined not for a class, but for the interface. It’s necessary to get the code and the type defined for the attribute. You can also describe the table where it’s stored as well, as a rule of join request to this table. In this case, the attribute address type will be saved in an ordinary flat table which is connected with shopping cart address table via address_id key, and entity_id key – with the order address table. Therefore, it’s necessary to create each table in Setup/InstallSchema.php class and implement the model and resource model for each table. We can write data to the table using the save_after event processors for the order and the shopping cart.

<event name="sales_quote_address_save_after">
        <observer name="ajaxcart" instance="Web4pro\Ajaxcart\Observer\Address" shared="false" />
    </event>
    <event name="sales_order_address_save_after">
        <observer name="ajaxcart" instance="Web4pro\Ajaxcart\Observer\Order\Address" shared="false" />
    </event>
namespace Web4pro\Ajaxcart\Observer;

class Address implements \Magento\Framework\Event\ObserverInterface {

    protected $_objectManager;

    public function __construct(\Magento\Framework\ObjectManagerInterface $objectManagerInterface){
        $this->_objectManager = $objectManagerInterface;
    }

    public function execute(\Magento\Framework\Event\Observer $observer){

        if($address = $observer->getEvent()->getQuoteAddress()){
           if($attributes = $address->getExtensionAttributes()){
               $customAddress = $this->_objectManager->create('\Web4pro\Ajaxcart\Model\Quote\Address');
               $customAddress->setType($attributes->getType())->setAddressId($address->getId())->save();
           }
        }
    }
} 

namespace Web4pro\Ajaxcart\Observer\Order;

class Address implements \Magento\Framework\Event\ObserverInterface {

    protected $_objectManager;

    public function __construct(\Magento\Framework\ObjectManagerInterface $objectManagerInterface){
        $this->_objectManager = $objectManagerInterface;
    }

    public function execute(\Magento\Framework\Event\Observer $observer){

        if($address = $observer->getEvent()->getAddress()){
            if($attributes = $address->getExtensionAttributes()){
                $customAddress = $this->_objectManager->create('\Web4pro\Ajaxcart\Model\Order\Address');
                $customAddress->setType($attributes->getType())->setId($address->getId())->save();
            }
        }
    }
}

As there are usually two addresses in the shopping cart and the order: shipping and delivery, both addresses are loaded with the collection. It’s pretty easy to add the attributes to the collection. You can do it using the load_before processor of the collection event via\Magento\Framework\Api\ExtensionAttribute\JoinProcessor object.  Let’s look at its implementation:

<event name="sales_quote_address_collection_load_before">
        <observer name="ajaxcart" instance="Web4pro\Ajaxcart\Observer\AddressCollectionLoad" shared="false" />
    </event>
    <event name="sales_order_address_collection_load_before">
        <observer name="ajaxcart" instance="Web4pro\Ajaxcart\Observer\Order\AddressCollectionLoad" shared="false" />
    </event>
namespace Web4pro\Ajaxcart\Observer;

class AddressCollectionLoad implements \Magento\Framework\Event\ObserverInterface {

    protected $_joinProcessor;

    public function __construct(\Magento\Framework\Api\ExtensionAttribute\JoinProcessor $joinProcessor){
        $this->_joinProcessor = $joinProcessor;
    }

    public function execute(\Magento\Framework\Event\Observer $observer){
        if($collection = $observer->getEvent()->getQuoteAddressCollection()){
            $this->_joinProcessor->process($collection);
        }
    }
} 

namespace Web4pro\Ajaxcart\Observer\Order;

class AddressCollectionLoad implements \Magento\Framework\Event\ObserverInterface {

    protected $_joinProcessor;

    public function __construct(\Magento\Framework\Api\ExtensionAttribute\JoinProcessor $joinProcessor){
        $this->_joinProcessor = $joinProcessor;
    }

    public function execute(\Magento\Framework\Event\Observer $observer){
        if($collection = $observer->getEvent()->getOrderAddressCollection()){
            $this->_joinProcessor->process($collection);
        }
    }
}

As a result, we can get the extension attributes after loading the collection  using _getExtensionAttributes() method which returns the entity that implements \Magento\Quote\Api\Data\AddressExtensionInterface for the shopping cart address and \Magento\Sales\Api\Data\OrderAddressExtensionInterface for the order address. The interface and the class which implements it are dynamic. And they are formed right on the go in var/generation directory on the base of the description in etc/extension_attributes.xml files of different modules for this type.

But there are some pitfalls in this implementation. Say, you successfully outputted the attribute to the checkout page (we’ll show how to do it below) and saved, for example, the shipping address. But in case we reload the checkout page or go to the shopping cart, we could get the empty checkout page without any content or the error message in the main log. Here is how it looks like:

Recoverable Error: Argument 1 passed to Magento\Quote\Model\Cart\Totals::setExtensionAttributes() must be an instance of Magento\Quote\Api\Data\TotalsExtensionInterface, instance of Magento\Quote\Api\Data\AddressExtension given, called in /var/www/brandrpm/vendor/magento/framework/Api/DataObjectHelper.php on line 127 and defined in /var/www/brandrpm/vendor/magento/module-quote/Model/Cart/Totals.php on line 592 [] []

We can find the reason for this error in the Magento\Quote\Model\Cart\CartTotalRepository class. This class implements Totals values load for display. It’s implemented the following way:

public function get($cartId)
    {
        /** @var \Magento\Quote\Model\Quote $quote */
        $quote = $this->quoteRepository->getActive($cartId);
        if ($quote->isVirtual()) {
            $addressTotalsData = $quote->getBillingAddress()->getData();
            $addressTotals = $quote->getBillingAddress()->getTotals();
        } else {
            $addressTotalsData = $quote->getShippingAddress()->getData();
            $addressTotals = $quote->getShippingAddress()->getTotals();
        }

        /** @var \Magento\Quote\Api\Data\TotalsInterface $quoteTotals */
        $quoteTotals = $this->totalsFactory->create();
        $this->dataObjectHelper->populateWithArray(
            $quoteTotals,
            $addressTotalsData,
            '\Magento\Quote\Api\Data\TotalsInterface'
        );
        $items = [];
        foreach ($quote->getAllVisibleItems() as $index => $item) {
            $items[$index] = $this->itemConverter->modelToDataObject($item);
        }
        $calculatedTotals = $this->totalsConverter->process($addressTotals);
        $quoteTotals->setTotalSegments($calculatedTotals);

        $amount = $quoteTotals->getGrandTotal() - $quoteTotals->getTaxAmount();
        $amount = $amount > 0 ? $amount : 0;
        $quoteTotals->setCouponCode($this->couponService->get($cartId));
        $quoteTotals->setGrandTotal($amount);
        $quoteTotals->setItems($items);
        $quoteTotals->setItemsQty($quote->getItemsQty());
        $quoteTotals->setBaseCurrencyCode($quote->getBaseCurrencyCode());
        $quoteTotals->setQuoteCurrencyCode($quote->getQuoteCurrencyCode());
        return $quoteTotals;
    }

As you can see, this method just extracts the _data array values from the address entity and puts them to the entity which implements \Magento\Quote\Api\Data\TotalsInterface. It’s done this way because Totals values are physically stored in the address table and uploaded to the address entity. And the extension entities attributes of various classes can implement different and even incompatible interfaces. By the way, the same would happen when copying extension attributes using etc/fieldset.xml. In our case the file will look the following way:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:DataObject/etc/fieldset.xsd">
    <scope id="global">
        <fieldset id="customer_address">
            <field name="type">
                <aspect name="to_quote_address" />
            </field>
        </fieldset>
        <fieldset id="sales_convert_quote_address">
            <field name="extension_attributes">
                <aspect name="to_customer_address" />
                <aspect name="to_order_address" />
            </field>
        </fieldset>
        <fieldset id="sales_convert_order_address">
            <field name="extension_attributes">
                 <aspect name="to_quote_address" />
            </field>
        </fieldset>
    </scope>
</config>

We can sort out the problem of type incompatibility using the plugin for populateWithArray() method.

<type name="Magento\Framework\Api\DataObjectHelper">
        <plugin name="move-extension-attributes" type="Web4pro\Ajaxcart\Model\Plugin" sortOrder="20"/>
    </type>
public function beforePopulateWithArray($helper,$dataObject, array $data, $interfaceName){
        switch($interfaceName){
            case '\Magento\Sales\Api\Data\OrderAddressInterface':
                if($data['extension_attributes'] instanceof \Magento\Quote\Api\Data\AddressExtensionInterface){
                    $data['extension_attributes'] = $data['extension_attributes']->__toArray();
                }
            break;
            case '\Magento\Customer\Api\Data\AddressInterface':
                if($data['extension_attributes'] instanceof \Magento\Quote\Api\Data\AddressExtensionInterface){
                    $data['extension_attributes'] = $data['extension_attributes']->__toArray();
                    if(isset($data['extension_attributes']['type'])){
                        $data['type'] = $data['extension_attributes']['type'];
                    }
                }
                break;
            case '\Magento\Quote\Api\Data\TotalsInterface':
                unset($data['extension_attributes']);
                break;
        }
        return array($dataObject,$data,$interfaceName);
    }

As the interface name of the source object is transmitted to _populateWithArray() method, we can check it and modify the data. We can get away extension attributes at all, as it’s done for \Magento\Quote\Api\Data\TotalsInterface in our case. But if we need any extension attributes for this interface, it’s better to use __toArray() method which any extension attribute entity has and which returns the array key – the extensions attribute value. If transmit this array to _populateWithArray() method, it will automatically write it to the extensions attribute value and transform it into the type of the relevant interface.

The question is: “Why Magento 2 developers haven’t included the automatic converting of extension attribute values to the array for the comfort transmitting between the entities?” – We can only suppose all extension attribute classes implement __toArray() method, which is implemented in Magento\Framework\Api\AbstractSimpleObject parent class. For \Magento\Quote\Api\Data\AddressExtensionInterface, we also saved the attribute value right to the $data array, because in this case this value will be written with _setCustomAttribute() method, and the entity of this interface supports all created EVAs.

You can look at the frontend implementation here. But we must admit that Mixin Javascript is implemented the next way:

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

    return function (setShippingInformationAction) {

        return wrapper.wrap(setShippingInformationAction, function (originalAction) {
            var shippingAddress = quote.shippingAddress();
            if (shippingAddress['extension_attributes'] === undefined) {
                shippingAddress['extension_attributes'] = {};
            }
            var tp = shippingAddress.customAttributes['type'];
            if(typeof tp=='object'){
                tp = tp.value;
            }
            shippingAddress['extension_attributes']['type'] = tp;
            // pass execution to original action ('Magento_Checkout/js/action/set-shipping-information')
            return originalAction();
        });
    };
});

ShippingAddress.customAttributes[‘type’] value checking also should be implemented. Because if the user who places the order has already given the address, and this address is already saved in the address book, the result is the entity in this variable. We must transmit only the value to the server. This variable will have the scalar type for a new address.

The value output on the order view page on admin panel is implemented with a simple plugin:

<type name="Magento\Sales\Block\Adminhtml\Order\View\Info">
        <plugin name="render-type" type="Web4pro\Ajaxcart\Model\Plugin" sortOrder="20"/>
    </type>
public function beforeGetFormattedAddress($block,$address){
        if($attributes = $address->getExtensionAttributes()){
            $address->setType($attributes->getType());
        }
        return array($address);
    }

After all, we must only add the {{var type}} variable to the address format in Magento configuration, and as a result, this field will be outputted.