Magento 2 Agregar lista desplegable al método de envío


16

Desarrollo el método de envío para alguna empresa de logística. Esta empresa tiene muchas oficinas donde el cliente puede obtener su pedido. Puedo obtener la lista de oficinas por ciudad en API, pero ahora no sé qué mejor represento este paso.

Por ahora, acabo de establecer una nueva \Magento\Quote\Model\Quote\Address\RateResult\Method en cada oficina en la ciudad, en la gran ciudad, cuenta> 100 y creo que no es muy bueno establecer 100 líneas en el pago.

Será un módulo público para un diseño de pago diferente, así que, ¿cómo puedo mostrar una lista desplegable con mi lista de oficinas y establecer el precio y el método después de que el usuario seleccione uno?


@Zefiryn Encontré esta publicación muy interesante, pero tengo una pregunta, si tengo que mostrar en la selección, no en las oficinas, sino en las tiendas que están dentro del módulo de Amasty, ¿cómo haría la segunda parte de su publicación? Quiero decir: ¿dónde está el lugar donde llamo al ayudante de Amasty para llenar el componente xml "vendor_carrier_form"? Gracias
maverickk89

Si tiene una nueva pregunta, hágalo haciendo clic en el botón Hacer pregunta . Incluya un enlace a esta pregunta si ayuda a proporcionar contexto. - De la opinión
Jai

esta no es una pregunta nueva, sino una variación de la forma en que Zefiryn ... porque usé la primera parte de la publicación tal como está
maverickk89

Respuestas:


17

El pago y envío de Magento no admite ningún tipo de formulario para el método de envío de datos adicionales. Pero proporciona un shippingAdditionalbloqueo en el proceso de pago que se puede usar para esto. La siguiente solución funcionará para el pago estándar de magento.

Primero preparemos nuestro contenedor donde podamos poner alguna forma. Para hacer esto, cree un archivo enview/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>

Ahora cree un archivo en el Vendor/Module/view/frontend/web/js/view/checkout/shipping/form.jsque se generará una plantilla de eliminación. Su contenido se ve así

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 archivo utiliza una plantilla de eliminación que debe colocarse en 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>

Ahora tenemos un campo de selección que será visible cuando nuestro método (definido por su código) se seleccione en la tabla de métodos de envío. Es hora de llenarlo con algunas opciones. Dado que los valores dependen de la dirección, la mejor manera es crear un punto final de descanso que proporcione las opciones disponibles. EnVendor/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>

Ahora defina la interfaz 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 interfaz para datos de oficina en Vendor\Module\Api\Data\OfficeInterface.php. Esta interfaz será utilizada por el módulo webapi para filtrar datos para la salida, por lo que debe definir lo que necesite agregar a la respuesta.

namespace Vendor\Module\Api\Data;

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

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

Tiempo para clases reales. Comience con la creación de preferencias para todas las interfaces enVendor/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>

Ahora cree una Vendor\Module\Model\OfficeManagement.phpclase que realmente haga la lógica de obtener los datos.

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

Y finalmente clase para OfficeInterfaceadentroVendor/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');
    }
}

Esto debería mostrar el campo de selección y actualizarlo cuando se cambia la dirección. Pero nos falta un elemento más para la manipulación frontend. Necesitamos crear una función que llame al punto final. La llamada a él ya está incluida Vendor/Module/view/frontend/web/js/view/checkout/shipping/form.jsy es una Vendor_Module/js/view/checkout/shipping/office-serviceclase a la que debe ir Vendor/Module/view/frontend/web/js/view/checkout/shipping/office-service.jscon el siguiente 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);
                        }
                    );
                }
            }
        };
    }
);

Utiliza 2 archivos js más. Vendor_Module/js/view/checkout/shipping/model/resource-url-managercrea una URL para el punto final y es bastante simple

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-registryEs una forma de mantener el resultado en el almacenamiento local. Su código es:

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, deberíamos tener a todos trabajando en la interfaz. Pero ahora hay otro problema por resolver. Dado que el proceso de pago no sabe nada sobre este formulario, no enviará el resultado de la selección al backend. Para que esto suceda, necesitamos usar la extension_attributesfunción. Esta es una forma en magento2 de informar al sistema que se espera que haya datos adicionales en las llamadas restantes. Sin él, magento filtraría esos datos y nunca alcanzarían el código.

Así que primero en Vendor/Module/etc/extension_attributes.xmldefinir:

<?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 ya está insertado en la solicitud form.jspor this.selectedOffice.subscribe()definición. Entonces, la configuración anterior solo la pasará en la entrada. Para buscarlo en el código, cree un complemento enVendor/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 de esa clase

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

Por supuesto, dónde guardará el valor depende completamente de sus requisitos. El código anterior requeriría la creación de columna adicional carrier_officeen quote_addressy sales_addressmesas y un evento (en 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>

Eso copiaría los datos guardados en la dirección de presupuesto a la dirección de ventas.

Escribí esto para mi módulo para INPOST portador de esmalte así que cambié algunos nombres que podrían romper el código, pero espero que esto le dará lo que necesita.

[EDITAR]

Modelo de operador preguntado 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;
    }
}

Gracias por su respuesta extendida, intentaré resolver mi problema utilizando su método y responderé con el resultado en estos días.
Siarhey Uchukhlebau

@Zefiryn He creado un método de envío personalizado, a continuación se mostrará un menú desplegable con los números de cuenta de envío del cliente (se ha creado un atributo de cliente personalizado), así que si tengo que mostrar este menú desplegable, ¿cuánto porcentaje de su código será útil? ¿Qué debo recoger del código que me proporcionó?
Shireen N

@shireen Yo diría que alrededor del 70%. Debe cambiar la parte donde recupera máquinas a números de cuenta. Entonces, la definición de la API será un poco diferente y es parte de ella
Zefiryn

He intentado este módulo ... pero no muestra ningún cambio, así que comparta el módulo de trabajo. Si lo hay
Sangan el

después de agregar un módulo exitoso ... en el proceso de pago ajax cargando continuamente ... en el error de la consola que se muestra a continuación: require.js: 166 Error no capturado: Error de script para: Vendor_Module / js / view / checkout / shipping / model / office-Registry. requirejs.org/docs/errors.html#scripterror
sangan

2

Estoy agregando una nueva respuesta para ampliar lo que ya se proporcionó anteriormente pero sin distorsionarlo.

Esta es la ruta que se QuoteAddressPluginestaba conectando:

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

El último método fue llamar, Magento\Quote\Model\Quote\Address::setShippingMethod()que en realidad era la llamada Magento\Quote\Model\Quote\Address::__call()que usé. En este momento encontré un lugar mejor para el complemento, es el Magento\Quote\Model\ShippingAssignment::setShipping()método. Entonces, la parte del complemento se puede reescribir en:

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

y el complemento en sí:

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

1

@ Zefiryn, me encontré con el problema con: quote.shippingAddress().extensionAttributes.carrier_office = office;

Cuando entro en la caja la primera vez (nueva ventana privada) como invitado (pero ocurre lo mismo con el cliente registrado), la oficina de atributos no se guarda en la base de datos después del primer "Siguiente". Aunque en la consola veo la salida correcta para:console.log(quote.shippingAddress().extensionAttributes.carrier_office);

Cuando regrese a la primera página de pago y seleccione Office nuevamente, se guardará. ¿Cuál podría ser la razón de este comportamiento?

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

pero sin éxito...


0

@ Zefiryn, ¿Puedes explicar en pocas palabras cómo funciona tu plugin anterior? Estoy un poco confundido porque, como sé, el método __call se ejecuta si intentamos ejecutar el método que no existe para un objeto en particular. Parece ser cierto porque en app / code / Magento / Quote / Model / Quote / Address.php no veo ese método, solo comente:

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

  1. ¿Por qué utilizas la intercepción cuando no hay implementación de métodos?
  2. A continuación veo $subject->setInpostMachine y $subject->getCarrierOffice(null);¿Significa que el método del complemento anterior se ejecutará nuevamente ya que no hay un método setInpostMachine () y getCarrierOffice () en Adress Class? A mí me parece un lazo.
  3. ¿Desde dónde ejecuta Magento setShippingMethod()? ¿Con qué frecuencia se usa este método? No encuentro intercepciones similares en el código de Magento.

Ok, entonces preparé la respuesta basada en un módulo que escribí para probar, usaba el campo inpost_machine, por lo que este no se cambió correctamente a carrier_office en este lugar. En segundo lugar, en el momento en que estaba desarrollando este módulo, no encontré un lugar donde pudiera obtener tanto el operador seleccionado como la dirección con los atributos de extensión enviados, excepto la setShippingMethodllamada al AddressInterfaceobjeto, y dado que no existe tal método, tuve que usar around__call para ver si setShippingMethodfue llamado o algún otro campo mágico. Ahora mismo he encontrado un lugar mejor y lo publicaré en una nueva respuesta.
Zefiryn
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.