Perché il totale di spedizione imposta il row_weight degli articoli dell'offerta di vendita su 0 se è attiva la spedizione gratuita?


21

Prefazione: serve sia come osservazione scritta dell'architettura di Magento per la comunità (e per me), sia come domanda reale. Stiamo lavorando con un'esperienza di carrello ed acquisto fortemente modificata, ma la radice di questo problema è nella logica di base di Magento.

sfondo

Abbiamo creato un coupon di spedizione gratuito utilizzando la funzionalità standard delle regole di prezzo del carrello. Non ci sono condizioni sul coupon e l'unica azione è quella Free Shippingimpostata For matching items only. Poiché non ci sono condizioni, questo verrà impostato free_shippingsu 1per tutti gli articoli di preventivo di vendita.

Come di consueto, abbiamo anche abilitato il metodo di spedizione Spedizione gratuita. Il Freeshippingmodello del corriere fornirà le tariffe ogni volta che la richiesta ha la spedizione gratuita o il totale parziale corrisponde o supera la soglia (ma non stiamo usando l'opzione soglia). Vedi Mage_Shipping_Model_Carrier_Freeshipping::collectRates:

$this->_updateFreeMethodQuote($request);

if (($request->getFreeShipping()) // <-- This is the condition we're relying on
    || ($request->getBaseSubtotalInclTax() >=
        $this->getConfigData('free_shipping_subtotal'))
) {
    /* Snip: Add $0.00 method to the result */
}

E si Mage_Shipping_Model_Carrier_Freeshipping::_updateFreeMethodQuotepresenta così:

protected function _updateFreeMethodQuote($request)
{
    $freeShipping = false;
    $items = $request->getAllItems();
    $c = count($items);
    for ($i = 0; $i < $c; $i++) {
        if ($items[$i]->getProduct() instanceof Mage_Catalog_Model_Product) {
            if ($items[$i]->getFreeShipping()) {
                $freeShipping = true;
            } else {
                return;
            }
        }
    }
    if ($freeShipping) {
        $request->setFreeShipping(true);
    }
}

Quindi, purché tutti gli articoli abbiano free_shippingimpostato un valore di verità (che a causa del coupon), dovremmo ottenere la spedizione gratuita. E lo facciamo!

Il problema

Tuttavia, c'è un grande effetto collaterale: tutti i metodi di spedizione che si basano su un articolo row_weight(come nel caso della nostra versione personalizzata del corriere FedEx) non calcoleranno le tariffe di spedizione appropriate perché ogni articolo row_weightè impostato su 0quando è attiva la spedizione gratuita.

È interessante notare che nessuno dei corrieri di default di Magento si affida effettivamente row_weight, ma ci arriveremo dopo aver capito perché / quando row_weightè impostato su0 .

Capire perché row_weightè impostato su0

Questa parte è stata in realtà abbastanza facile da scavare. Si verifica una grande parte dei calcoli di spedizione Mage_Sales_Model_Quote_Address_Total_Shipping::collect, inclusa l'impostazione row_weightdi 0:

public function collect(Mage_Sales_Model_Quote_Address $address)
{
    parent::collect($address);

    foreach ($items as $item) {
        /* Snip: Handling virtual items and parent items */

        if ($item->getHasChildren() && $item->isShipSeparately()) {
            /* Snip: Handling items with children */
        }
        else {
            if (!$item->getProduct()->isVirtual()) {
                $addressQty += $item->getQty();
            }
            $itemWeight = $item->getWeight();
            $rowWeight  = $itemWeight*$item->getQty();
            $addressWeight+= $rowWeight;
            if ($freeAddress || $item->getFreeShipping()===true) {
                $rowWeight = 0;
            } elseif (is_numeric($item->getFreeShipping())) {
                $freeQty = $item->getFreeShipping();
                if ($item->getQty()>$freeQty) {
                    $rowWeight = $itemWeight*($item->getQty()-$freeQty);
                }
                else {
                    $rowWeight = 0;
                }
            }
            $freeMethodWeight+= $rowWeight;
            $item->setRowWeight($rowWeight);
        }
    }

Perché questo non influisce sui gestori predefiniti di Magento

Se fate una ricerca regex per /row_?weight/i(ad esempio getRowWeight, setRowWeight, setData('row_weight'), etc.) Mage_Shipping(vettori semplici) eMage_Usa (FedEx, UPS, e di alcuni altri vettori), non si apre. Perché? Poiché i corrieri predefiniti utilizzano il peso totale dell'indirizzo, non i pesi dei singoli articoli.

Ad esempio, diamo un'occhiata a Mage_Usa_Model_Shipping_Carrier_Fedex::setRequest:

public function setRequest(Mage_Shipping_Model_Rate_Request $request)
{
    $this->_request = $request;

    $r = new Varien_Object();

    /* Snip */

    $weight = $this->getTotalNumOfBoxes($request->getPackageWeight());
    $r->setWeight($weight);
    if ($request->getFreeMethodWeight()!= $request->getPackageWeight()) {
        $r->setFreeMethodWeight($request->getFreeMethodWeight());
    }

E da dove viene la richiesta ottenere il peso del pacchetto? La risposta è in Mage_Sales_Model_Quote_Address::requestShippingRates:

public function requestShippingRates(Mage_Sales_Model_Quote_Item_Abstract $item = null)
{
    /** @var $request Mage_Shipping_Model_Rate_Request */
    $request = Mage::getModel('shipping/rate_request');
    /* Snip */
    $request->setPackageWeight($item ? $item->getRowWeight() : $this->getWeight());

Possiamo ignorare l'uso di $item->getRowWeight()qui perché requestShippingRatesviene chiamato senza fornire un elemento specifico come parametro in Mage_Sales_Model_Quote_Address_Total_Shipping::collect:

public function collect(Mage_Sales_Model_Quote_Address $address)
{
    parent::collect($address);

    foreach ($items as $item) {
        /* Snip: Handling virtual items and parent items */

        if ($item->getHasChildren() && $item->isShipSeparately()) {
            /* Snip: Handling items with children */
        }
        else {
            if (!$item->getProduct()->isVirtual()) {
                $addressQty += $item->getQty();
            }
            $itemWeight = $item->getWeight();
            $rowWeight  = $itemWeight*$item->getQty();
            $addressWeight+= $rowWeight;
            if ($freeAddress || $item->getFreeShipping()===true) {
                $rowWeight = 0;
            } elseif (is_numeric($item->getFreeShipping())) {
                $freeQty = $item->getFreeShipping();
                if ($item->getQty()>$freeQty) {
                    $rowWeight = $itemWeight*($item->getQty()-$freeQty);
                }
                else {
                    $rowWeight = 0;
                }
            }
            $freeMethodWeight+= $rowWeight;
            $item->setRowWeight($rowWeight);
        }
    }

    $address->setWeight($addressWeight);
    $address->setFreeMethodWeight($freeMethodWeight);

    $address->collectShippingRates();

Questo dovrebbe sembrare familiare, poiché si tratta dello stesso posto in cui row_weightè impostato ogni articolo 0se è attiva la spedizione gratuita. Si noti come $addressWeightsomma di ogni elemento $rowWeight, ma ciò che è stato fatto prima row_weightè impostato su0 .

Fondamentalmente, il peso dell'indirizzo sarà sempre il peso totale di tutti gli articoli, indipendentemente dal free_shippingvalore di ciascun articolo. Poiché i gestori predefiniti di Magento si basano solo sul peso dell'indirizzo, il problema con row_weightnon si presenta.

Allora perché ne abbiamo bisogno row_weight

Abbiamo bisogno row_weight perché abbiamo personalizzato il corriere FedEx di Magento per calcolare tariffe separate per articoli che provengono da origini diverse, anche se vanno alla stessa destinazione (e fanno quindi parte dello stesso indirizzo). Ad esempio, se vivi in ​​NJ, è più economico (e più veloce) spedire un articolo da NJ che da CA - e se hai articoli sia di NJ che di CA nel tuo ordine, puoi vedere il costo (e la stima data di consegna) di ogni spedizione.

Tutto sommato, sembra che possiamo facilmente aggirare questo problema ignorando row_weighte utilizzando weight * qtydirettamente. Ma ci porta a:

La domanda

Perché il Shippingtotale imposta gli row_weightarticoli del preventivo di vendita su 0se è attiva la spedizione gratuita? Questo non sembra essere utilizzato da nessuna parte.

Ulteriori osservazioni

Ho trascurato di menzionare che in row_weightrealtà potrebbe essere diverso da zero, ma comunque inferiore a weight * qty, se free_shippingè un numero anziché true. Presumo che lo scopo sia quello di fornire una soluzione a uno scenario come questo:

Ho 3 articoli dello stesso prodotto nel mio carrello, ogni articolo pesa 2 libbre. Applico un coupon di spedizione gratuito, ma è limitato a una quantità di 2, quindi si applica solo a 2 degli articoli. Ora, quando guarderò le tariffe di spedizione, guarderò le tariffe di spedizione per 2 + 0 + 0 libbre invece che 2 + 2 + 2 libbre.

Questo sembra avere senso, ma ci sono due problemi principali:

  • Nessuno dei vettori Magento predefiniti funziona in questo modo (usano l'indirizzo peso totale, vedi sopra).

  • Anche se alcuni dei corrieri lavorassero in questo modo, ciò significherebbe che potrei scegliere qualsiasi metodo di spedizione (ad es. Spedizione durante la notte) e pagare solo il peso di 1 articolo - il che significa che il commerciante dovrebbe coprire il costo degli altri 2 articoli . Spetterebbe al commerciante in qualche modo capire che ho pagato solo il peso di 1 articolo, quindi spedire gli altri 2 articoli utilizzando un metodo più economico, creando in modo efficace una discrepanza tra ciò che Magento visualizza e il modo in cui gli articoli erano effettivamente spedito fuori.


Eh, niente acquirenti? :)
Agop,

Così tanti voti positivi, così poca discussione! Tale è la vita dello sviluppo di Magento.
Agop,

Risposte:


2

Ho pensato che avrei preso una pugnalata a questo ...;)

Piuttosto una domanda interessante che hai posto qui, quindi ecco perché penso che l'abbia fatto, tuttavia sto ancora lavorando per rintracciare quando questo particolare caso entra in gioco.

Andando oltre il metodo del corriere USPS, sembra che le richieste internazionali alle loro API forniscano pesi dettagliati per ciascun prodotto. Questo è l'unico operatore telefonico che riesco a trovare. Trova il metodo completo di seguito e la sezione evidenziata di seguito.

protected function _formIntlShipmentRequest(Varien_Object $request)
    {
        $packageParams = $request->getPackageParams();
        $height = $packageParams->getHeight();
        $width = $packageParams->getWidth();
        $length = $packageParams->getLength();
        $girth = $packageParams->getGirth();
        $packageWeight = $request->getPackageWeight();
        if ($packageParams->getWeightUnits() != Zend_Measure_Weight::POUND) {
            $packageWeight = Mage::helper('usa')->convertMeasureWeight(
                $request->getPackageWeight(),
                $packageParams->getWeightUnits(),
                Zend_Measure_Weight::POUND
            );
        }
        if ($packageParams->getDimensionUnits() != Zend_Measure_Length::INCH) {
            $length = round(Mage::helper('usa')->convertMeasureDimension(
                $packageParams->getLength(),
                $packageParams->getDimensionUnits(),
                Zend_Measure_Length::INCH
            ));
            $width = round(Mage::helper('usa')->convertMeasureDimension(
                $packageParams->getWidth(),
                $packageParams->getDimensionUnits(),
                Zend_Measure_Length::INCH
            ));
            $height = round(Mage::helper('usa')->convertMeasureDimension(
                $packageParams->getHeight(),
                $packageParams->getDimensionUnits(),
                Zend_Measure_Length::INCH
            ));
        }
        if ($packageParams->getGirthDimensionUnits() != Zend_Measure_Length::INCH) {
            $girth = round(Mage::helper('usa')->convertMeasureDimension(
                $packageParams->getGirth(),
                $packageParams->getGirthDimensionUnits(),
                Zend_Measure_Length::INCH
            ));
        }

        $container = $request->getPackagingType();
        switch ($container) {
            case 'VARIABLE':
                $container = 'VARIABLE';
                break;
            case 'FLAT RATE ENVELOPE':
                $container = 'FLATRATEENV';
                break;
            case 'FLAT RATE BOX':
                $container = 'FLATRATEBOX';
                break;
            case 'RECTANGULAR':
                $container = 'RECTANGULAR';
                break;
            case 'NONRECTANGULAR':
                $container = 'NONRECTANGULAR';
                break;
            default:
                $container = 'VARIABLE';
        }
        $shippingMethod = $request->getShippingMethod();
        list($fromZip5, $fromZip4) = $this->_parseZip($request->getShipperAddressPostalCode());

        // the wrap node needs for remove xml declaration above
        $xmlWrap = new SimpleXMLElement('<?xml version = "1.0" encoding = "UTF-8"?><wrap/>');
        $method = '';
        $service = $this->getCode('service_to_code', $shippingMethod);
        if ($service == 'Priority') {
            $method = 'Priority';
            $rootNode = 'PriorityMailIntlRequest';
            $xml = $xmlWrap->addChild($rootNode);
        } else if ($service == 'First Class') {
            $method = 'FirstClass';
            $rootNode = 'FirstClassMailIntlRequest';
            $xml = $xmlWrap->addChild($rootNode);
        } else {
            $method = 'Express';
            $rootNode = 'ExpressMailIntlRequest';
            $xml = $xmlWrap->addChild($rootNode);
        }

        $xml->addAttribute('USERID', $this->getConfigData('userid'));
        $xml->addAttribute('PASSWORD', $this->getConfigData('password'));
        $xml->addChild('Option');
        $xml->addChild('Revision', self::DEFAULT_REVISION);
        $xml->addChild('ImageParameters');
        $xml->addChild('FromFirstName', $request->getShipperContactPersonFirstName());
        $xml->addChild('FromLastName', $request->getShipperContactPersonLastName());
        $xml->addChild('FromFirm', $request->getShipperContactCompanyName());
        $xml->addChild('FromAddress1', $request->getShipperAddressStreet2());
        $xml->addChild('FromAddress2', $request->getShipperAddressStreet1());
        $xml->addChild('FromCity', $request->getShipperAddressCity());
        $xml->addChild('FromState', $request->getShipperAddressStateOrProvinceCode());
        $xml->addChild('FromZip5', $fromZip5);
        $xml->addChild('FromZip4', $fromZip4);
        $xml->addChild('FromPhone', $request->getShipperContactPhoneNumber());
        if ($method != 'FirstClass') {
            if ($request->getReferenceData()) {
                $referenceData = $request->getReferenceData() . ' P' . $request->getPackageId();
            } else {
                $referenceData = $request->getOrderShipment()->getOrder()->getIncrementId()
                                 . ' P'
                                 . $request->getPackageId();
            }
            $xml->addChild('FromCustomsReference', 'Order #' . $referenceData);
        }
        $xml->addChild('ToFirstName', $request->getRecipientContactPersonFirstName());
        $xml->addChild('ToLastName', $request->getRecipientContactPersonLastName());
        $xml->addChild('ToFirm', $request->getRecipientContactCompanyName());
        $xml->addChild('ToAddress1', $request->getRecipientAddressStreet1());
        $xml->addChild('ToAddress2', $request->getRecipientAddressStreet2());
        $xml->addChild('ToCity', $request->getRecipientAddressCity());
        $xml->addChild('ToProvince', $request->getRecipientAddressStateOrProvinceCode());
        $xml->addChild('ToCountry', $this->_getCountryName($request->getRecipientAddressCountryCode()));
        $xml->addChild('ToPostalCode', $request->getRecipientAddressPostalCode());
        $xml->addChild('ToPOBoxFlag', 'N');
        $xml->addChild('ToPhone', $request->getRecipientContactPhoneNumber());
        $xml->addChild('ToFax');
        $xml->addChild('ToEmail');
        if ($method != 'FirstClass') {
            $xml->addChild('NonDeliveryOption', 'Return');
        }
        if ($method == 'FirstClass') {
            if (stripos($shippingMethod, 'Letter') !== false) {
                $xml->addChild('FirstClassMailType', 'LETTER');
            } else if (stripos($shippingMethod, 'Flat') !== false) {
                $xml->addChild('FirstClassMailType', 'FLAT');
            } else{
                $xml->addChild('FirstClassMailType', 'PARCEL');
            }
        }
        if ($method != 'FirstClass') {
            $xml->addChild('Container', $container);
        }
        $shippingContents = $xml->addChild('ShippingContents');
        $packageItems = $request->getPackageItems();
        // get countries of manufacture
        $countriesOfManufacture = array();
        $productIds = array();
        foreach ($packageItems as $itemShipment) {
                $item = new Varien_Object();
                $item->setData($itemShipment);

                $productIds[]= $item->getProductId();
        }
        $productCollection = Mage::getResourceModel('catalog/product_collection')
            ->addStoreFilter($request->getStoreId())
            ->addFieldToFilter('entity_id', array('in' => $productIds))
            ->addAttributeToSelect('country_of_manufacture');
        foreach ($productCollection as $product) {
            $countriesOfManufacture[$product->getId()] = $product->getCountryOfManufacture();
        }

        $packagePoundsWeight = $packageOuncesWeight = 0;
        // for ItemDetail
        foreach ($packageItems as $itemShipment) {
            $item = new Varien_Object();
            $item->setData($itemShipment);

            $itemWeight = $item->getWeight() * $item->getQty();
            if ($packageParams->getWeightUnits() != Zend_Measure_Weight::POUND) {
                $itemWeight = Mage::helper('usa')->convertMeasureWeight(
                    $itemWeight,
                    $packageParams->getWeightUnits(),
                    Zend_Measure_Weight::POUND
                );
            }
            if (!empty($countriesOfManufacture[$item->getProductId()])) {
                $countryOfManufacture = $this->_getCountryName(
                    $countriesOfManufacture[$item->getProductId()]
                );
            } else {
                $countryOfManufacture = '';
            }
            $itemDetail = $shippingContents->addChild('ItemDetail');
            $itemDetail->addChild('Description', $item->getName());
            $ceiledQty = ceil($item->getQty());
            if ($ceiledQty < 1) {
                $ceiledQty = 1;
            }
            $individualItemWeight = $itemWeight / $ceiledQty;
            $itemDetail->addChild('Quantity', $ceiledQty);
            $itemDetail->addChild('Value', $item->getCustomsValue() * $item->getQty());
            list($individualPoundsWeight, $individualOuncesWeight) = $this->_convertPoundOunces($individualItemWeight);
            $itemDetail->addChild('NetPounds', $individualPoundsWeight);
            $itemDetail->addChild('NetOunces', $individualOuncesWeight);
            $itemDetail->addChild('HSTariffNumber', 0);
            $itemDetail->addChild('CountryOfOrigin', $countryOfManufacture);

            list($itemPoundsWeight, $itemOuncesWeight) = $this->_convertPoundOunces($itemWeight);
            $packagePoundsWeight += $itemPoundsWeight;
            $packageOuncesWeight += $itemOuncesWeight;
        }
        $additionalPackagePoundsWeight = floor($packageOuncesWeight / self::OUNCES_POUND);
        $packagePoundsWeight += $additionalPackagePoundsWeight;
        $packageOuncesWeight -= $additionalPackagePoundsWeight * self::OUNCES_POUND;
        if ($packagePoundsWeight + $packageOuncesWeight / self::OUNCES_POUND < $packageWeight) {
            list($packagePoundsWeight, $packageOuncesWeight) = $this->_convertPoundOunces($packageWeight);
        }

        $xml->addChild('GrossPounds', $packagePoundsWeight);
        $xml->addChild('GrossOunces', $packageOuncesWeight);
        if ($packageParams->getContentType() == 'OTHER' && $packageParams->getContentTypeOther() != null) {
            $xml->addChild('ContentType', $packageParams->getContentType());
            $xml->addChild('ContentTypeOther ', $packageParams->getContentTypeOther());
        } else {
            $xml->addChild('ContentType', $packageParams->getContentType());
        }

        $xml->addChild('Agreement', 'y');
        $xml->addChild('ImageType', 'PDF');
        $xml->addChild('ImageLayout', 'ALLINONEFILE');
        if ($method == 'FirstClass') {
            $xml->addChild('Container', $container);
        }
        // set size
        if ($packageParams->getSize()) {
            $xml->addChild('Size', $packageParams->getSize());
        }
        // set dimensions
        $xml->addChild('Length', $length);
        $xml->addChild('Width', $width);
        $xml->addChild('Height', $height);
        if ($girth) {
            $xml->addChild('Girth', $girth);
        }

        $xml = $xmlWrap->{$rootNode}->asXML();
        return $xml;
    }

Sezione particolare:

$packagePoundsWeight = $packageOuncesWeight = 0;
        // for ItemDetail
        foreach ($packageItems as $itemShipment) {
            $item = new Varien_Object();
            $item->setData($itemShipment);

            $itemWeight = $item->getWeight() * $item->getQty();
            if ($packageParams->getWeightUnits() != Zend_Measure_Weight::POUND) {
                $itemWeight = Mage::helper('usa')->convertMeasureWeight(
                    $itemWeight,
                    $packageParams->getWeightUnits(),
                    Zend_Measure_Weight::POUND
                );
            }
            if (!empty($countriesOfManufacture[$item->getProductId()])) {
                $countryOfManufacture = $this->_getCountryName(
                    $countriesOfManufacture[$item->getProductId()]
                );
            } else {
                $countryOfManufacture = '';
            }
            $itemDetail = $shippingContents->addChild('ItemDetail');
            $itemDetail->addChild('Description', $item->getName());
            $ceiledQty = ceil($item->getQty());
            if ($ceiledQty < 1) {
                $ceiledQty = 1;
            }
            $individualItemWeight = $itemWeight / $ceiledQty;
            $itemDetail->addChild('Quantity', $ceiledQty);
            $itemDetail->addChild('Value', $item->getCustomsValue() * $item->getQty());
            list($individualPoundsWeight, $individualOuncesWeight) = $this->_convertPoundOunces($individualItemWeight);
            $itemDetail->addChild('NetPounds', $individualPoundsWeight);
            $itemDetail->addChild('NetOunces', $individualOuncesWeight);
            $itemDetail->addChild('HSTariffNumber', 0);
            $itemDetail->addChild('CountryOfOrigin', $countryOfManufacture);

            list($itemPoundsWeight, $itemOuncesWeight) = $this->_convertPoundOunces($itemWeight);
            $packagePoundsWeight += $itemPoundsWeight;
            $packageOuncesWeight += $itemOuncesWeight;
        }

A mio avviso, Magento sta ricalcolando il peso del pacco in base ai pesi effettivi dell'articolo e non sfruttando il peso dell'indirizzo. Mi chiedo se gli articoli del pacchetto che vengono passati non abbiano il loro peso azzerato, perché ciò porterebbe a pesi degli articoli falsi considerando che la risposta alle tariffe si baserebbe su dati di peso errato.

Girato nel buio però.


1

Ho avuto un colpo anche su questo e ho trovato qualcosa di interessante questa linea di codice

$item->setRowWeight($rowWeight);

è stato introdotto sulla versione 1.1.5 prima che la funzione fosse così.

Magento Mirror Import Magento Release 1.1.5 - Shipping.php

Magento Mirror Import Magento Release 1.1.1 - Shipping.php

            else {
            if (!$item->getProduct()->getTypeInstance()->isVirtual()) {
                $addressQty += $item->getQty();
            }
            $itemWeight = $item->getWeight();
            $rowWeight  = $itemWeight*$item->getQty();
            $addressWeight+= $rowWeight;
            if ($freeAddress || $item->getFreeShipping()===true) {
                $rowWeight = 0;
            } elseif (is_numeric($item->getFreeShipping())) {
                $freeQty = $item->getFreeShipping();
                if ($item->getQty()>$freeQty) {
                    $rowWeight = $itemWeight*($item->getQty()-$freeQty);
                }
                else {
                    $rowWeight = 0;
                }
            }
            $freeMethodWeight+= $rowWeight;
        }
    }

La mia comprensione è che Mage_Sales_Model_Quote_Address_Total_Shipping :: raccogliere la necessità di calcolare / aggiornare $ addressWeight e $ freeMethodWeight

    $addressWeight      = $address->getWeight();
    $freeMethodWeight   = $address->getFreeMethodWeight();

e $ item-> setRowWeight shod non possono essere utilizzati qui perché sono correlati all'elemento e non al preventivo e all'indirizzo.

La mia scommessa è che questo è un errore. Probabilmente row_total non è mai stato progettato per essere utilizzato nei metodi di spedizione, per questo motivo i moduli predefiniti non lo utilizzano.

Non sono stato in grado di rintracciare alcun registro delle modifiche che possa spiegare perché questo è stato introdotto nella v 1.1.5.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.