Magento 2 Добавить раскрывающийся список к способу доставки

16

Я разрабатываю способ доставки для какой-то логистической компании. Эта компания имеет много офисов, где клиент может получить свой заказ. Я могу получить список офисов по городам в API но я не знаю, как лучше представить этот шаг?

На данный момент я просто установил новый \Magento\Quote\Model\Quote\Address\RateResult\Method для каждого офиса в городе, в большом городе это число> 100 и я думаю, что не очень хорошо устанавливать 100 строк в кассе.

Это будет общедоступный модуль для другого дизайна оформления заказа, так как я могу отобразить рядом с выбранным моим способом доставки какой-либо выпадающий список со списком офисов и установить цену и метод после того, как пользователь выберет один.

Джеймс Мориарти
источник
@Zefiryn Я нашел этот пост очень интересным, но у меня есть вопрос, если я должен показывать в избранных не офисы, а магазины, которые находятся внутри модуля Amasty, как бы я сделал вторую часть вашего поста? Я имею в виду: где находится место, где я вызываю помощника Amasty для заполнения xml-компонента vendor_carrier_form? Спасибо
maverickk89
Если у вас есть новый вопрос, задайте его, нажав кнопку « Задать вопрос» . Включите ссылку на этот вопрос, если это помогает обеспечить контекст. - Из обзора
Jai
это не новый вопрос, а вариант использования Zefiryn ... потому что я использовал первую часть поста, как она есть
maverickk89

Ответы:

17

Magento Checkout не поддерживает какие-либо формы для дополнительных данных метода доставки. Но это обеспечивает shippingAdditionalблок в кассе, который может быть использован для этого. Следующее решение будет работать для стандартной проверки magento.

Сначала давайте подготовим наш контейнер, куда мы можем поместить некоторую форму. Для этого создайте файл вview/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>

Теперь создайте файл, в Vendor/Module/view/frontend/web/js/view/checkout/shipping/form.jsкотором будет отображаться шаблон для нокаута. Его содержание выглядит так

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();
        }
    });
});

Этот файл использует шаблон нокаута, который должен быть помещен в 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>

Теперь у нас есть поле выбора, которое будет видно, когда наш метод (определенный его кодом) будет выбран в таблице методов доставки. Время заполнить его некоторыми опциями. Поскольку значения зависят от адреса, лучшим способом является создание конечной точки отдыха, которая будет предоставлять доступные параметры. ВVendor/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>

Теперь определите интерфейс Vendor/Module/Api/OfficeManagementInterface.phpкак

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);
}

Определить интерфейс для офисных данных в Vendor\Module\Api\Data\OfficeInterface.php. Этот интерфейс будет использоваться модулем webapi для фильтрации данных для вывода, поэтому вам нужно определить все, что нужно добавить в ответ.

namespace Vendor\Module\Api\Data;

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

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

Время для реальных занятий. Начните с создания настроек для всех интерфейсов вVendor/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>

Теперь создайте Vendor\Module\Model\OfficeManagement.phpкласс, который будет фактически выполнять логику извлечения данных.

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;
    }
}

И, наконец, класс для OfficeInterfaceвVendor/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');
    }
}

Это должно показать поле выбора и обновить его при изменении адреса. Но мы упускаем еще один элемент для манипулирования интерфейсом. Нам нужно создать функцию, которая будет вызывать конечную точку. Вызов к нему уже включен, Vendor/Module/view/frontend/web/js/view/checkout/shipping/form.jsи это Vendor_Module/js/view/checkout/shipping/office-serviceкласс, который должен идти Vendor/Module/view/frontend/web/js/view/checkout/shipping/office-service.jsсо следующим кодом:

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);
                        }
                    );
                }
            }
        };
    }
);

Он использует еще 2 файла JS. Vendor_Module/js/view/checkout/shipping/model/resource-url-managerсоздает ссылку на конечную точку и довольно просто

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способ сохранить результат в локальном хранилище Его код:

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;
            }
        };
    }
);

Итак, мы должны все работать на фронтенде. Но теперь есть еще одна проблема, которую нужно решить. Поскольку Checkout ничего не знает об этой форме, он не отправит результат выбора бэкэнду. Чтобы это произошло, нам нужно использоватьextension_attributes функцию. В magento2 это способ информировать систему о том, что в остальных вызовах ожидаются дополнительные данные. Без этого magento отфильтровывал бы эти данные, и они никогда не достигли бы кода.

Итак, сначала Vendor/Module/etc/extension_attributes.xmlопределимся:

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

Это значение уже вставлено в запрос form.jsпо this.selectedOffice.subscribe()определению. Таким образом, приведенная выше конфигурация будет проходить только при входе. Чтобы получить его в коде, создайте плагин вVendor/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>

Внутри этого класса

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;
    }
}

Конечно, где вы сохраните ценность, полностью зависит от ваших требований. Приведенный выше код потребует создания дополнительного столбца carrier_officeв quote_addressи sales_addressтаблиц и события (в 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>

Это скопировало бы данные, сохраненные в адресе цитаты, в адрес продажи.

Я написал это для моего модуля для польского носителя InPost, поэтому я изменил некоторые имена, которые могут нарушить код, но я надеюсь, что это даст вам то, что вам нужно.

[РЕДАКТИРОВАТЬ]

Модель носителя по запросу @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
источник
Спасибо за ваш расширенный ответ, я постараюсь решить мою проблему, используя ваш метод, и я отвечу с результатом в эти дни.
Сергей Учухлебау
@Zefiryn Я создал собственный метод доставки, ниже которого будет отображаться раскрывающееся меню с номерами учетных записей клиентов (там создан настраиваемый атрибут клиента), поэтому, если мне нужно отобразить это раскрывающееся меню, какой процент вашего кода будет полезен? Что я должен получить из кода, который вы предоставили?
Ширин N
@shireen Я бы сказал, около 70%. Вам нужно изменить часть, где он выбирает машины на номера счетов. Таким образом, определение API будет немного отличаться, и JS часть его
Zefiryn
я попробовал этот модуль ... но его не показывает каких - либо изменений , так пожалуйста , поделитесь Рабочими module.if любой
Сянган
после добавления успешного модуля .. в проверке загрузка ajax непрерывно .. в консольной ошибке, показанной как показано ниже: require.js: 166 Uncaught Error: Ошибка скрипта для: Vendor_Module / js / view / checkout / shipping / model / office-registry. requirejs.org/docs/errors.html#scripterror
Санган
2

Я добавляю новый ответ, чтобы расширить то, что уже было предоставлено ранее, но не искажая его.

Это маршрут, который QuoteAddressPluginбыл подключен к:

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() 

Последний метод был вызовом, Magento\Quote\Model\Quote\Address::setShippingMethod()который фактически был вызовом, Magento\Quote\Model\Quote\Address::__call()который я использовал. Прямо сейчас я нашел лучшее место для плагина, это Magento\Quote\Model\ShippingAssignment::setShipping()метод. Так что часть плагина может быть переписана на:

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

и сам плагин:

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
источник
1

@Zefiryn, я столкнулся с проблемой: quote.shippingAddress().extensionAttributes.carrier_office = office;

Когда я вхожу в кассу в первый раз (новое личное окно) в качестве гостя (но то же самое происходит с зарегистрированным клиентом), атрибут office не сохраняется в базе данных после первого «Next». Хотя в консоли я вижу правильный вывод для:console.log(quote.shippingAddress().extensionAttributes.carrier_office);

Когда я возвращаюсь на первую страницу оформления заказа и снова выбираю офис, он сохраняется. Что может быть причиной такого поведения?

Я пытался использовать: address.trigger_reload = new Date().getTime(); rateRegistry.set(address.getKey(), null); rateRegistry.set(address.getCacheKey(), null); quote.shippingAddress(address);

но без успеха ...

user2089098
источник
0

@Zefiryn, не могли бы вы в двух словах объяснить, как работает ваш плагин выше? Я немного запутался, потому что, как я знаю, метод __call выполняется, если мы пытаемся выполнить метод execute, который не существует для определенного объекта. Кажется, это правда, потому что в app / code / Magento / Quote / Model / Quote / Address.php я не вижу такой метод - только комментарий:

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

  1. Почему вы используете перехват, когда нет реализации метода?
  2. Далее я вижу, $subject->setInpostMachineи $subject->getCarrierOffice(null);означает ли это, что вышеуказанный метод плагина будет выполнен снова, так как в классе Adress нет метода setInpostMachine () и getCarrierOffice ()? Это выглядит как петля для меня.
  3. Откуда Magento казнить setShippingMethod()? Как обычно этот метод используется? Я не могу найти никаких схожих перехватов в коде Magento.
user2089098
источник
Итак, я подготовил ответ, основываясь на модуле, который я написал для тестирования, он использовал поле inpost_machine, так что этот просто не был правильно изменен на carrier_office в этом месте. Во-вторых, в то время, когда я разрабатывал этот модуль, я не нашел места, где я мог бы получить как выбранный носитель, так и адрес с отправленными атрибутами расширения, кроме setShippingMethodвызова AddressInterfaceобъекта, и, поскольку такого метода нет, мне пришлось использовать около__call, чтобы увидеть, setShippingMethodНазывалось или какое-то другое магическое поле. Прямо сейчас я нашел лучшее место, и я отправлю это в новом ответе.
Зефирин