Pourquoi le total des frais de port définit-il le poids de ligne des articles du devis de vente sur 0 si la livraison gratuite est en vigueur?

21

Préface: Ceci est destiné à servir à la fois d'observation écrite de l'architecture de Magento pour la communauté (et moi-même), ainsi que d'une question réelle. Nous travaillons avec une expérience de panier et de paiement fortement modifiée, mais la racine de ce problème est dans la logique principale de Magento.

Contexte

Nous avons créé un coupon de livraison gratuite en utilisant la fonctionnalité standard de règles de prix du panier. Il n'y a aucune condition sur le coupon, et la seule action est celle qui Free Shippingest définie sur For matching items only. Puisqu'il n'y a pas de conditions, cela s'appliquera free_shippingà 1tous les articles du devis de vente.

Comme d'habitude, nous avons également activé la méthode d'expédition Livraison gratuite. Le Freeshippingmodèle de transporteur fournira des tarifs chaque fois que la demande a la livraison gratuite ou que le sous-total correspond ou dépasse le seuil (mais nous n'utilisons pas l'option de seuil). Voir 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 */
}

Et Mage_Shipping_Model_Carrier_Freeshipping::_updateFreeMethodQuoteressemble à ceci:

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

Donc, tant que tous les articles ont free_shippingune valeur vraie (ce qu'ils seront, en raison du coupon), nous devrions obtenir la livraison gratuite. Et nous le faisons!

Le problème

Cependant, il y a un effet secondaire majeur: toutes les méthodes d'expédition qui reposent sur un article row_weight(comme c'est le cas avec notre version personnalisée du transporteur FedEx) ne parviendront pas à calculer les tarifs d'expédition appropriés parce que chaque article row_weightest défini 0lorsque la livraison gratuite est active.

Chose intéressante, aucun des transporteurs maritimes par défaut de Magento ne compte vraiment row_weight, mais nous y reviendrons après avoir compris pourquoi / quand row_weightest réglé sur 0.

Comprendre pourquoi row_weightest réglé sur0

Cette partie était en fait assez facile à déterrer. Une grande partie des calculs d'expédition se produisent Mage_Sales_Model_Quote_Address_Total_Shipping::collect, notamment la définition row_weightde 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);
        }
    }

Pourquoi cela n'affecte pas les opérateurs par défaut de Magento

Si vous effectuez une recherche regex pour /row_?weight/i(par exemple getRowWeight, setRowWeight, setData('row_weight'), etc.) Mage_Shipping(transporteurs simples) et Mage_Usa(FedEx, UPS, et d'autres transporteurs), rien n'apparaît. Pourquoi? Étant donné que les transporteurs par défaut utilisent le poids total de l'adresse, pas les poids individuels des articles.

Par exemple, regardons 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());
    }

Et d'où la demande obtient-elle le poids du colis? La réponse est 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());

Nous pouvons ignorer l'utilisation d' $item->getRowWeight()ici car requestShippingRatesest appelé sans fournir un élément spécifique en tant que paramètre dans 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();

Cela devrait vous sembler familier, car c'est le même endroit où chaque article row_weightest défini 0si la livraison gratuite est en vigueur. Remarquez comment $addressWeightrésume chaque élément $rowWeight, mais cela est fait avant row_weightest défini sur0 .

Fondamentalement, le poids de l'adresse sera toujours le poids total de tous les articles, quelle que soit la free_shippingvaleur de chaque article. Étant donné que les transporteurs par défaut de Magento ne dépendent que du poids de l'adresse, le problème avec row_weightn'apparaît pas.

Alors pourquoi avons-nous besoin row_weight

Nous en avons besoin row_weightparce que nous avons personnalisé le transporteur FedEx de Magento pour calculer des tarifs distincts pour les articles qui viennent de différentes origines, même s'ils vont vers la même destination (et font donc partie de la même adresse). Par exemple, si vous vivez au NJ, il est moins cher (et plus rapide) d'expédier un article du NJ que de CA - et si vous avez des articles du NJ et de CA dans votre commande, vous pouvez voir le coût (et l'estimation date de livraison) de chaque expédition.

Dans l'ensemble, il semble que nous pouvons facilement contourner ce problème en ignorant row_weightet en utilisant weight * qtydirectement. Mais cela nous amène à:

La question

Pourquoi le Shippingtotal définit-il les row_weightarticles du devis de vente 0si la livraison gratuite est en vigueur? Cela ne semble être utilisé nulle part.

Observations complémentaires

J'ai négligé de mentionner que cela row_weightpourrait en fait être non nul, mais toujours inférieur à weight * qty, si free_shippingc'est un nombre au lieu de true. Je suppose que le but de ceci est de fournir une solution à un scénario comme celui-ci:

J'ai 3 articles du même produit dans mon panier, chaque article pesant 2 livres. J'applique un coupon de livraison gratuite, mais il est limité à une quantité de 2, il ne s'applique donc qu'à 2 des articles. Maintenant, quand je regarde les tarifs d'expédition, je vais regarder les tarifs d'expédition pour 2 lb (2 + 0 + 0) plutôt que 6 lb (2 + 2 + 2).

Cela semble logique, mais cela pose deux problèmes majeurs:

  • Aucun des transporteurs Magento par défaut ne fonctionne comme ça (ils utilisent le poids total de l'adresse, voir ci-dessus).

  • Même si certains des transporteurs fonctionnaient comme cela, cela signifierait que je pourrais choisir n'importe quelle méthode d'expédition (par exemple, expédition de nuit) et ne payer que le poids de 1 article - ce qui signifie que le commerçant devrait couvrir le coût des 2 autres articles. . Il appartiendrait au commerçant de comprendre en quelque sorte que je n'ai payé que le poids d'un article, puis d'expédier les 2 autres articles en utilisant une méthode plus rentable, créant effectivement un décalage entre ce que Magento affiche et la façon dont les articles étaient réellement expédiés.

Agop
la source
Hé, pas de preneurs? :)
Agop
Tant de votes positifs, si peu de discussion! Telle est la vie du développement de Magento.
Agop

Réponses:

2

Je pensais que je tenterais ça ...;)

Une question assez intéressante que vous avez posée ici, alors pourquoi je pense qu'ils l'ont fait, mais je continue de chercher à savoir quand ce cas particulier entre en jeu.

En passant par la méthode du transporteur USPS, il semble que les demandes internationales adressées à leur API fournissent des poids détaillés pour chaque produit. C'est le seul transporteur que je puisse trouver qui fait cela. Trouvez la méthode complète ci-dessous et la section en surbrillance en dessous.

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

Section particulière:

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

Pour moi, il semble que Magento recalcule le poids du colis en fonction des poids réels des articles et ne tire pas parti du poids de l'adresse. Je me demande si les articles du colis passés n'ont pas leur poids à zéro, car cela conduirait à de faux poids des articles étant donné que la réponse des taux serait basée sur de mauvaises données de poids.

Tourné dans l'obscurité cependant.

Daniel Kenney
la source
1

J'ai aussi eu un coup de poignard à ce sujet et j'ai trouvé quelque chose d'intéressant cette ligne de code

$item->setRowWeight($rowWeight);

a été introduit sur la version 1.1.5 avant que la fonction soit comme ça.

Magento Mirror Importation Magento version 1.1.5 - Shipping.php

Magento Mirror Importation Magento version 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;
        }
    }

Ma compréhension est que Mage_Sales_Model_Quote_Address_Total_Shipping :: collect doit calculer / mettre à jour $ addressWeight et $ freeMethodWeight

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

et $ item-> setRowWeight ne doit pas être utilisé ici car il est lié à l'article et non au devis et à l'adresse.

Je parie que c'est une erreur. Probablement, row_total n'a jamais été destiné à être utilisé dans les méthodes d'expédition, c'est pourquoi les modules par défaut ne l'utilisent pas.

Je n'ai pas pu retrouver de journal des modifications pouvant expliquer pourquoi cela a été introduit dans la version 1.1.5.

Daniel Yovchev
la source