Magento 2 Adicione a lista suspensa ao método de envio

16

Eu desenvolvo o método de envio para alguma empresa de logística. Esta empresa possui muitos escritórios onde o cliente pode receber seu pedido. Posso obter a lista de escritórios por cidade na API mas agora não estou representando melhor essa etapa?

Por enquanto, acabei de definir um novo \Magento\Quote\Model\Quote\Address\RateResult\Method em todos os escritórios da cidade, na cidade grande conta> 100 e acho que não é muito bom definir 100 linhas no checkout.

Será um módulo público para diferentes designs de checkout. Como posso renderizar perto do meu método de envio selecionado alguma lista suspensa com lista de escritórios e definir preço e método após o usuário selecionar um.

Jamess Moriarty
fonte
@ Zefiryn achei este post muito interessante, mas tenho uma pergunta: se eu tiver que mostrar no select não os escritórios, mas as lojas que estão dentro do módulo do Amasty, como eu faria a segunda parte do seu post? Quero dizer: onde é o local onde chamo o auxiliar do Amasty para preencher o componente xml "vendor_carrier_form"? Graças
maverickk89
Se você tiver uma nova pergunta, faça-o clicando no botão Fazer pergunta . Inclua um link para esta pergunta se ela ajudar a fornecer contexto. - Do comentário
Jai
isso não é uma questão nova, mas uma variação do caminho usado por Zefiryn ... porque eu usei a primeira parte do post como é
maverickk89

Respostas:

17

O Magento checkout não suporta nenhum tipo de formulário para dados adicionais do método de envio. Mas fornece shippingAdditionalbloqueio no check-out que pode ser usado para isso. A solução a seguir funcionará no checkout padrão do magento.

Primeiro vamos preparar nosso contêiner onde podemos colocar alguma forma. Para isso, crie um arquivo emview/frontend/layout/checkout_index_index.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="checkout.root">
            <arguments>
                <argument name="jsLayout" xsi:type="array">
                    <item name="components" xsi:type="array">
                        <item name="checkout" xsi:type="array">
                            <item name="children" xsi:type="array">
                                <item name="steps" xsi:type="array">
                                    <item name="children" xsi:type="array">
                                        <item name="shipping-step" xsi:type="array">
                                            <item name="children" xsi:type="array">
                                                <item name="shippingAddress" xsi:type="array">
                                                    <item name="children" xsi:type="array">
                                                        <item name="shippingAdditional" xsi:type="array">
                                                            <item name="component" xsi:type="string">uiComponent</item>
                                                            <item name="displayArea" xsi:type="string">shippingAdditional</item>
                                                            <item name="children" xsi:type="array">
                                                                <item name="vendor_carrier_form" xsi:type="array">
                                                                    <item name="component" xsi:type="string">Vendor_Module/js/view/checkout/shipping/form</item>
                                                                </item>
                                                            </item>
                                                        </item>
                                                    </item>
                                                </item>
                                            </item>
                                        </item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

Agora crie um arquivo no Vendor/Module/view/frontend/web/js/view/checkout/shipping/form.jsqual renderizará um modelo de knockout. Seu conteúdo fica assim

define([
    'jquery',
    'ko',
    'uiComponent',
    'Magento_Checkout/js/model/quote',
    'Magento_Checkout/js/model/shipping-service',
    'Vendor_Module/js/view/checkout/shipping/office-service',
    'mage/translate',
], function ($, ko, Component, quote, shippingService, officeService, t) {
    'use strict';

    return Component.extend({
        defaults: {
            template: 'Vendor_Module/checkout/shipping/form'
        },

        initialize: function (config) {
            this.offices = ko.observableArray();
            this.selectedOffice = ko.observable();
            this._super();
        },

        initObservable: function () {
            this._super();

            this.showOfficeSelection = ko.computed(function() {
                return this.ofices().length != 0
            }, this);

            this.selectedMethod = ko.computed(function() {
                var method = quote.shippingMethod();
                var selectedMethod = method != null ? method.carrier_code + '_' + method.method_code : null;
                return selectedMethod;
            }, this);

            quote.shippingMethod.subscribe(function(method) {
                var selectedMethod = method != null ? method.carrier_code + '_' + method.method_code : null;
                if (selectedMethod == 'carrier_method') {
                    this.reloadOffices();
                }
            }, this);

            this.selectedOffice.subscribe(function(office) {
                if (quote.shippingAddress().extensionAttributes == undefined) {
                    quote.shippingAddress().extensionAttributes = {};
                }
                quote.shippingAddress().extensionAttributes.carrier_office = office;
            });


            return this;
        },

        setOfficeList: function(list) {
            this.offices(list);
        },

        reloadOffices: function() {
            officeService.getOfficeList(quote.shippingAddress(), this);
            var defaultOffice = this.offices()[0];
            if (defaultOffice) {
                this.selectedOffice(defaultOffice);
            }
        },

        getOffice: function() {
            var office;
            if (this.selectedOffice()) {
                for (var i in this.offices()) {
                    var m = this.offices()[i];
                    if (m.name == this.selectedOffice()) {
                        office = m;
                    }
                }
            }
            else {
                office = this.offices()[0];
            }

            return office;
        },

        initSelector: function() {
            var startOffice = this.getOffice();
        }
    });
});

Este arquivo usa um modelo de knockout que deve ser colocado em Vendor/Module/view/frontend/web/template/checkout/shipping/form.html

<div id="carrier-office-list-wrapper" data-bind="visible: selectedMethod() == 'carrier_method'">
    <p data-bind="visible: !showOfficeSelection(), i18n: 'Please provide postcode to see nearest offices'"></p>
    <div data-bind="visible: showOfficeSelection()">
        <p>
            <span data-bind="i18n: 'Select pickup office.'"></span>
        </p>
        <select id="carrier-office-list" data-bind="options: offices(),
                                            value: selectedOffice,
                                            optionsValue: 'name',
                                            optionsText: function(item){return item.location + ' (' + item.name +')';}">
        </select>
    </div>
</div>

Agora temos um campo de seleção que ficará visível quando nosso método (definido por seu código) for selecionado na tabela de métodos de envio. Hora de preenchê-lo com algumas opções. Como os valores dependem do endereço, a melhor maneira é criar o ponto de extremidade restante que fornecerá as opções disponíveis. DentroVendor/Module/etc/webapi.xml

<?xml version="1.0"?>

<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">

    <!-- Managing Office List on Checkout page -->
    <route url="/V1/module/get-office-list/:postcode/:city" method="GET">
        <service class="Vendor\Module\Api\OfficeManagementInterface" method="fetchOffices"/>
        <resources>
            <resource ref="anonymous" />
        </resources>
    </route>
</routes>

Agora defina a interface Vendor/Module/Api/OfficeManagementInterface.phpcomo

namespace Vendor\Module\Api;

interface OfficeManagementInterface
{

    /**
     * Find offices for the customer
     *
     * @param string $postcode
     * @param string $city
     * @return \Vendor\Module\Api\Data\OfficeInterface[]
     */
    public function fetchOffices($postcode, $city);
}

Definir interface para dados do escritório em Vendor\Module\Api\Data\OfficeInterface.php. Essa interface será usada pelo módulo webapi para filtrar dados para a saída, portanto, você precisa definir o que precisar adicionar à resposta.

namespace Vendor\Module\Api\Data;

/**
 * Office Interface
 */
interface OfficeInterface
{
    /**
     * @return string
     */
    public function getName();

    /**
     * @return string
     */
    public function getLocation();
}

Hora das aulas reais. Comece com a criação de preferências para todas as interfaces noVendor/Module/etc/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">
    <preference for="Vendor\Module\Api\OfficeManagementInterface" type="Vendor\Module\Model\OfficeManagement" />
    <preference for="Vendor\Module\Api\Data\OfficeInterface" type="Vendor\Module\Model\Office" />
</config>

Agora crie uma Vendor\Module\Model\OfficeManagement.phpclasse que realmente fará a lógica de buscar os dados.

namespace Vednor\Module\Model;

use Vednor\Module\Api\OfficeManagementInterface;
use Vednor\Module\Api\Data\OfficeInterfaceFactory;

class OfficeManagement implements OfficeManagementInterface
{
    protected $officeFactory;

    /**
     * OfficeManagement constructor.
     * @param OfficeInterfaceFactory $officeInterfaceFactory
     */
    public function __construct(OfficeInterfaceFactory $officeInterfaceFactory)
    {
        $this->officeFactory = $officeInterfaceFactory;
    }

    /**
     * Get offices for the given postcode and city
     *
     * @param string $postcode
     * @param string $limit
     * @return \Vendor\Module\Api\Data\OfficeInterface[]
     */
    public function fetchOffices($postcode, $city)
    {
        $result = [];
        for($i = 0, $i < 4;$i++) {
            $office = $this->officeFactory->create();
            $office->setName("Office {$i}");
            $office->setLocation("Address {$i}");
            $result[] = $office;
        }

        return $result;
    }
}

E, finalmente, aula para OfficeInterfaceemVendor/Module/Model/Office.php

namespace Vendor\Module\Model;

use Magento\Framework\DataObject;
use Vendor\Module\Api\Data\OfficeInterface;

class Office extends DataObject implements OfficeInterface
{
    /**
     * @return string
     */
    public function getName()
    {
        return (string)$this->_getData('name');
    }

    /**
     * @return string
     */
    public function getLocation()
    {
        return (string)$this->_getData('location');
    }
}

Isso deve mostrar o campo de seleção e atualizá-lo quando o endereço for alterado. Mas estamos perdendo mais um elemento para manipulação de front-end. Precisamos criar uma função que chamará o terminal. A chamada para ele já está incluída Vendor/Module/view/frontend/web/js/view/checkout/shipping/form.jse é a Vendor_Module/js/view/checkout/shipping/office-serviceclasse que deve seguir Vendor/Module/view/frontend/web/js/view/checkout/shipping/office-service.jscom o seguinte código:

define(
    [
        'Vendor_Module/js/view/checkout/shipping/model/resource-url-manager',
        'Magento_Checkout/js/model/quote',
        'Magento_Customer/js/model/customer',
        'mage/storage',
        'Magento_Checkout/js/model/shipping-service',
        'Vendor_Module/js/view/checkout/shipping/model/office-registry',
        'Magento_Checkout/js/model/error-processor'
    ],
    function (resourceUrlManager, quote, customer, storage, shippingService, officeRegistry, errorProcessor) {
        'use strict';

        return {
            /**
             * Get nearest machine list for specified address
             * @param {Object} address
             */
            getOfficeList: function (address, form) {
                shippingService.isLoading(true);
                var cacheKey = address.getCacheKey(),
                    cache = officeRegistry.get(cacheKey),
                    serviceUrl = resourceUrlManager.getUrlForOfficeList(quote);

                if (cache) {
                    form.setOfficeList(cache);
                    shippingService.isLoading(false);
                } else {
                    storage.get(
                        serviceUrl, false
                    ).done(
                        function (result) {
                            officeRegistry.set(cacheKey, result);
                            form.setOfficeList(result);
                        }
                    ).fail(
                        function (response) {
                            errorProcessor.process(response);
                        }
                    ).always(
                        function () {
                            shippingService.isLoading(false);
                        }
                    );
                }
            }
        };
    }
);

Ele usa mais 2 arquivos js. Vendor_Module/js/view/checkout/shipping/model/resource-url-managercria um URL para o terminal e é bastante simples

define(
    [
        'Magento_Customer/js/model/customer',
        'Magento_Checkout/js/model/quote',
        'Magento_Checkout/js/model/url-builder',
        'mageUtils'
    ],
    function(customer, quote, urlBuilder, utils) {
        "use strict";
        return {
            getUrlForOfficeList: function(quote, limit) {
                var params = {postcode: quote.shippingAddress().postcode, city: quote.shippingAddress().city};
                var urls = {
                    'default': '/module/get-office-list/:postcode/:city'
                };
                return this.getUrl(urls, params);
            },

            /** Get url for service */
            getUrl: function(urls, urlParams) {
                var url;

                if (utils.isEmpty(urls)) {
                    return 'Provided service call does not exist.';
                }

                if (!utils.isEmpty(urls['default'])) {
                    url = urls['default'];
                } else {
                    url = urls[this.getCheckoutMethod()];
                }
                return urlBuilder.createUrl(url, urlParams);
            },

            getCheckoutMethod: function() {
                return customer.isLoggedIn() ? 'customer' : 'guest';
            }
        };
    }
);

Vendor_Module/js/view/checkout/shipping/model/office-registryé uma maneira de manter o resultado no armazenamento local. Seu código é:

define(
    [],
    function() {
        "use strict";
        var cache = [];
        return {
            get: function(addressKey) {
                if (cache[addressKey]) {
                    return cache[addressKey];
                }
                return false;
            },
            set: function(addressKey, data) {
                cache[addressKey] = data;
            }
        };
    }
);

Ok, então todos nós devemos trabalhar no frontend. Mas agora há outro problema a ser resolvido. Como o checkout não sabe nada sobre este formulário, ele não envia o resultado da seleção para o back-end. Para fazer isso acontecer, precisamos usarextension_attributes recurso. Esta é uma maneira no magento2 de informar o sistema que alguns dados adicionais devem estar nas chamadas restantes. Sem ele, o magento filtraria esses dados e eles nunca alcançariam o código.

Então, primeiro, Vendor/Module/etc/extension_attributes.xmldefina:

<?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="carrier_office" type="string"/>
    </extension_attributes>
</config>

Este valor já está inserido na solicitação form.jspor this.selectedOffice.subscribe()definição. Portanto, a configuração acima só passará na entrada. Para buscá-lo no código, crie um plugin noVendor/Module/etc/di.xml

<type name="Magento\Quote\Model\Quote\Address">
    <plugin name="inpost-address" type="Vendor\Module\Quote\AddressPlugin" sortOrder="1" disabled="false"/>
</type>

Dentro dessa classe

namespace Vendor\Module\Plugin\Quote;

use Magento\Quote\Model\Quote\Address;
use Vendor\Module\Model\Carrier;

class AddressPlugin
{
    /**
     * Hook into setShippingMethod.
     * As this is magic function processed by __call method we need to hook around __call
     * to get the name of the called method. after__call does not provide this information.
     *
     * @param Address $subject
     * @param callable $proceed
     * @param string $method
     * @param mixed $vars
     * @return Address
     */
    public function around__call($subject, $proceed, $method, $vars)
    {
        $result = $proceed($method, $vars);
        if ($method == 'setShippingMethod'
            && $vars[0] == Carrier::CARRIER_CODE.'_'.Carrier::METHOD_CODE
            && $subject->getExtensionAttributes()
            && $subject->getExtensionAttributes()->getCarrierOffice()
        ) {
            $subject->setCarrierOffice($subject->getExtensionAttributes()->getCarrierOffice());
        }
        elseif (
            $method == 'setShippingMethod'
            && $vars[0] != Carrier::CARRIER_CODE.'_'.Carrier::METHOD_CODE
        ) {
            //reset office when changing shipping method
            $subject->getCarrierOffice(null);
        }
        return $result;
    }
}

Obviamente, onde você salvará o valor depende inteiramente de seus requisitos. O código acima exigiria a criação de coluna adicional carrier_officeno quote_addresse sales_addressmesas e um evento (em Vendor/Module/etc/events.xml)

<?xml version="1.0"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="sales_model_service_quote_submit_before">
        <observer name="copy_carrier_office" instance="Vendor\Module\Observer\Model\Order" />
    </event>
</config>

Isso copiaria os dados salvos no endereço de cotação no endereço de vendas.

Eu escrevi isso para o meu módulo para a operadora polonesa InPost, então mudei alguns nomes que podem quebrar o código, mas espero que isso lhe dê o que você precisa.

[EDITAR]

Modelo de transportadora solicitado por @sangan

namespace Vendor\Module\Model;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Phrase;
use Magento\Quote\Model\Quote\Address\RateRequest;
use Magento\Shipping\Model\Carrier\AbstractCarrier;
use Magento\Shipping\Model\Carrier\CarrierInterface;
use Magento\Shipping\Model\Simplexml\ElementFactory;

class Carrier extends AbstractCarrier implements CarrierInterface
{
    const CARRIER_CODE = 'mycarier';

    const METHOD_CODE = 'mymethod';

    /** @var string */
    protected $_code = self::CARRIER_CODE;

    /** @var bool */
    protected $_isFixed = true;

    /**
     * Prepare stores to show on frontend
     *
     * @param RateRequest $request
     * @return \Magento\Framework\DataObject|bool|null
     */
    public function collectRates(RateRequest $request)
    {
        if (!$this->getConfigData('active')) {
            return false;
        }

        /** @var \Magento\Shipping\Model\Rate\Result $result */
        $result = $this->_rateFactory->create();

        /** @var \Magento\Quote\Model\Quote\Address\RateResult\Method $method */
        $method = $this->_rateMethodFactory->create();
        $method->setCarrier($this->_code);
        $method->setCarrierTitle($this->getConfigData('title'));

        $price = $this->getFinalPriceWithHandlingFee(0);
        $method->setMethod(self::METHOD_CODE);
        $method->setMethodTitle(new Phrase('MyMethod'));
        $method->setPrice($price);
        $method->setCost($price);
        $result->append($method);;

        return $result;
    }


    /**
     * @return array
     */
    public function getAllowedMethods()
    {
        $methods = [
            'mymethod' => new Phrase('MyMethod')
        ];
        return $methods;
    }
}
Zefiryn
fonte
Obrigado por sua resposta estendida, tentarei resolver meu problema usando seu método e responderei com o resultado nos dias de hoje.
Siarhey Uchukhlebau
@ Zefiryn Criei um método de envio personalizado, abaixo dele será exibido um menu suspenso com os números da conta de envio do cliente (existe um atributo personalizado do cliente criado). Se eu tiver que exibir esse menu suspenso, quanto por cento do seu código será útil? O que devo pegar no código que você forneceu?
Shireen N #
@shireen, eu diria cerca de 70%. Você precisa alterar a parte em que ele busca as máquinas para os números das contas. Então definição api será diferent slighlty e parte js dele
Zefiryn
Eu tentei este módulo ... mas não mostrando quaisquer alterações por favor compartilhar o trabalho module.if qualquer
Sangan
depois de adicionar o módulo bem-sucedido .. no check-out ajax carregando continuamente .. no erro do console, como mostrado abaixo: require.js: 166 Erro não capturado: Erro de script para: Vendor_Module / js / view / checkout / shipping / model / office-registry. requirejs.org/docs/errors.html#scripterror
respondeu em
2

Estou adicionando uma nova resposta para expandir o que já foi fornecido anteriormente, mas sem distorcê-lo.

Esta é a rota que QuoteAddressPluginfoi conectada:

1. Magento\Checkout\Api\ShippingInformationManagementInterface::saveAddressInformation()
2. Magento\Quote\Model\QuoteRepository::save() 
3. Magento\Quote\Model\QuoteRepository\SaveHandler::save() 
4. Magento\Quote\Model\QuoteRepository\SaveHandler::processShippingAssignment() 
5. Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister::save()
6. Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor::save()
7. Magento\Quote\Model\Quote\ShippingAssignment\ShippingProcessor::save()
8. Magento\Quote\Model\ShippingMethodManagement::apply() 

O último método foi chamar o Magento\Quote\Model\Quote\Address::setShippingMethod()que realmente era chamado pelo Magento\Quote\Model\Quote\Address::__call()qual eu usei. Agora eu encontrei um lugar melhor para o plugin, é o Magento\Quote\Model\ShippingAssignment::setShipping()método. Portanto, a parte do plug-in pode ser reescrita para:

<type name="Magento\Quote\Model\ShippingAssignment">
    <plugin name="carrier-office-plugin" type="Vendor\Module\Plugin\Quote\ShippingAssignmentPlugin" sortOrder="1" disabled="false"/>
</type>

e o próprio plugin:

namespace Vednor\Module\Plugin\Quote;

use Magento\Quote\Api\Data\AddressInterface;
use Magento\Quote\Api\Data\ShippingInterface;
use Magento\Quote\Model\ShippingAssignment;
use Vendor\Module\Model\Carrier;

/**
 * ShippingAssignmentPlugin
 */
class ShippingAssignmentPlugin
{
    /**
     * Hook into setShipping.
     *
     * @param ShippingAssignment $subject
     * @param ShippingInterface $value
     * @return Address
     */
    public function beforeSetShipping($subject, ShippingInterface $value)
    {
        $method = $value->getMethod();
        /** @var AddressInterface $address */
        $address = $value->getAddress();
        if ($method === Carrier::CARRIER_CODE.'_'.Carrier::METHOD_CODE
            && $address->getExtensionAttributes()
            && $address->getExtensionAttributes()->getCarrierOffice()
        ) {
            $address->setCarrierOffice($address->getExtensionAttributes()->getCarrierOffice());
        }
        elseif ($method !== Carrier::CARRIER_CODE.'_'.Carrier::METHOD_CODE) {
            //reset inpost machine when changing shipping method
            $address->setCarrierOffice(null);
        }
        return [$value];
    }
}
Zefiryn
fonte
1

@ Zefiryn, me deparei com o problema com: quote.shippingAddress().extensionAttributes.carrier_office = office;

Quando entro no check-out pela primeira vez (nova janela privada) como hóspede (mas o mesmo ocorre com o cliente registrado), o escritório de atributos não é salvo no banco de dados após o primeiro "Avançar". Embora no console eu veja a saída correta para:console.log(quote.shippingAddress().extensionAttributes.carrier_office);

Quando volto à primeira página de check-out e seleciono o escritório novamente, ele é salvo. Qual poderia ser a razão desse comportamento?

Eu tentei usar: address.trigger_reload = new Date().getTime(); rateRegistry.set(address.getKey(), null); rateRegistry.set(address.getCacheKey(), null); quote.shippingAddress(address);

mas sem sucesso ...

user2089098
fonte
0

@ Zefiryn, você pode explicar em poucas palavras como o seu plugin acima funciona? Estou um pouco confuso porque, como eu sei, o método __call é executado se tentarmos executar o método que não existe para um objeto específico. Parece ser verdade porque em app / code / Magento / Quote / Model / Quote / Address.php eu não vejo esse método - apenas comente:

/** * Sales Quote address model ... * @method Address setShippingMethod(string $value)

  1. Por que você usa a interceptação quando não há implementação de método?
  2. A seguir, vejo $subject->setInpostMachinee $subject->getCarrierOffice(null);significa que o método do plug-in acima será executado novamente, pois não existe o método setInpostMachine () e getCarrierOffice () na classe Adress? Parece loop para mim.
  3. De onde o Magento executa setShippingMethod()? Como normalmente esse método é usado? Não consigo encontrar nenhuma interceptação semelhante no código Magento.
user2089098
fonte
Ok, então eu preparei a resposta com base em um módulo que escrevi para teste, ele usou o campo inpost_machine, portanto este simplesmente não foi alterado corretamente para carrier_office neste local. Segundo, no momento em que desenvolvi esse módulo, não encontrei um local onde pudesse obter a operadora e o endereço selecionados com os atributos de extensão enviados, exceto a setShippingMethodchamada no AddressInterfaceobjeto e, como não existe esse método, tive que usar around__call para verificar se setShippingMethodfoi chamado ou algum outro campo mágico. No momento, encontrei um lugar melhor e vou publicá-lo em nova resposta.
Zefiryn