Programmatically Creating Shipments

I'll give it a shot. Let's take them one at a time:

Method 1

$converter=Mage::getModel('sales/convert_order');
$shipment=$converter->toShipment($order);

$converter above is loaded from the class Mage_Sales_Model_Convert_Order, which uses a core helper called copyFieldset to copy order details into a shipment object. $order has to be of type array or Varien_Object.

This method is actually at the core of Method 3, as it uses Mage::getModel('sales/convert_order') in its constructor call.

Key differentiator of this method - it can take an array or an object $order and generate a basic $shipment object. It is a lower-level method used exclusively by the methods you put forth in Method 2, Method 3.

Method 2

 $shipment = Mage::getModel('sales/service_order', $order)
                            ->prepareShipment($this->_getItemQtys($order));

This seems to be the most popular way in Magento's Core of generating a shipment as it is used in both Shipment and Invoice controllers. $order is used as a constructor argument to the instantiation of Mage_Sales_Model_Service_Order, setting it as a protected property on the object.

You're then calling prepareShipment and passing a quantity. As this method uses the converter class from Method 1, you needn't specify more details such as order items pass item shipment qty details in the prepareShipment argument, called here with $this->_getItemQtys. To use this on your own context, all you need to do is pass the quantity of items in an array with the following format:

array(
  'order_item_id'=>$qty,
  'order_item_id'=>$qty,
  'order_item_id'=>$qty
)

Key differentiator of this method - it gives you back a $shipment object, but with all items converted on it. It's plug-and-play.

Method 3

I could not find evidence of using this method in the Core. It looks like a hack, to be honest. Here's the method:

$itemQty =  $order->getItemsCollection()->count();
$shipment = Mage::getModel('sales/service_order', $order)->prepareShipment($itemQty);
$shipment = new Mage_Sales_Model_Order_Shipment_Api();
$shipmentId = $shipment->create($orderId);

Step 1 is exactly the same as Method 2 above. No difference. However, you get back a $shipment object, which is replaced by a direct insantiation of Mage_Sales_Model_Order_Shipment_Api. This is non-standard. The best-practice way of getting a shipment Api object would be to call Mage::getModel('sales/order_shipment_api').

Next, it uses that overwritten, new Shipment API object to create a shipment from an $orderId variable that hasn't been defined in your code. Again, this seems like a workaround.

Looking at Mage_Sales_Model_Order_Shipment_Api::create(), it seems like a one-stop-shop for generating a shipment as the most basic details needed to create the shipment is only an order increment_id.

This is a hack that shouldn't be used by any module or extension. This API is meant to be consumed by features exposed via XML RPC / SOAP API requests and is intentionally basic to eliminate multiple step API requests.

Eventually Method 3 gets to the nitty-gritty, though, and via a call to Mage_Sales_Model_Order, it calls prepareShipment, which is a higher-order abstraction for the familiar Method 2 above:

public function prepareShipment($qtys = array())
{
    $shipment = Mage::getModel('sales/service_order', $this)->prepareShipment($qtys);
    return $shipment;
}

Key differentiator here - if you need a shipment, don't mind hacks, and only have an increment_id - use this method. Also useful information if you prefer to handle this via the SOAP API.

I hope that helps.


The key thing here is that methods 1 and 2 don't work...

I agree with @philwinkle though, method 3 is hacky. The API functions shouldn't really be called in a non-API context. You never know what future releases may bring to break this kind of code.

So what does that leave? Well, methods 1 and 2 aren't broken exactly. It's just that they only do part of the job. Here's what they should look like:

Note: for brevity, the following code snippets will add all eligible items to the shipment. If you just want to ship part of an order, you'll have to modify certain parts of the code - hopefully I've given you enough to go on, though.

Method 1

If you look at the code in app/code/core/Mage/Sales/Model/Order/Shipment/Api.php (as used in method 3) you'll see that in addition to $convertor->toShipment($order) it also calls $item = $convertor->itemToShipmentItem($orderItem), $item->setQty($qty) and $shipment->addItem($item) for every eligible order item. Yep, Magento really is that lazy, you have to coax it through every. Single. Step. Then you have to jump through a few more hoops to actually save the shipment in the database.

So method 1 should look like this:

$convertor = Mage::getModel('sales/convert_order');
$shipment = $convertor->toShipment($order);
foreach ($order->getAllItems() as $orderItem) {
    if ($orderItem->getQtyToShip() && !$orderItem->getIsVirtual()) {
        $item = $convertor->itemToShipmentItem($orderItem);
        $item->setQty($orderItem->getQtyToShip());
        $shipment->addItem($item);
    }
}
$shipment->register();
$order->setIsInProcess(true);
Mage::getModel('core/resource_transaction')
         ->addObject($shipment)
         ->addObject($order))
         ->save();

Method 2

First off, you have a call to $this->_getItemQtys() which will of course only work in certain classes (ones that have or inherit a _getItemQtys function, natch). So that needs to change, and as with method 1, you also need to flesh out the process.

Looking in app/code/core/Mage/Adminhtml/controllers/Sales/Order/ShipmentController.php it's a slightly better situation with this approach - it seems the items are converted along with the shipment itself. But you do still just get back a transient object, that you have to save to the database yourself, like so:

$itemQtys = array();
foreach ($order->getAllItems() as $orderItem) {
    if ($orderItem->getQtyToShip() && !$orderItem->getIsVirtual()) {
        $itemQtys[$orderItem->getId()] = $orderItem->getQtyToShip();
    }
}
$shipment = Mage::getModel('sales/service_order', $order)->prepareShipment($itemQtys);
$shipment->register();
$order->setIsInProcess(true);
Mage::getModel('core/resource_transaction')
         ->addObject($shipment)
         ->addObject($order)
         ->save();

I'd also recommend adding a little error-checking in there, e.g. to make sure your shipment actually contains any items before you register() it.

Which is best?

I'd say it's a matter of opinion. I haven't done any benchmark tests but I'm pretty confident the difference in speed between the two methods would be negligible. As for code size & readability there's not much between them.

I do like method 2 for not having to explicitly convert all the items in the order, but it does still require you to go through them to extract the quantities. For a nice small code footprint, method 3 would be my favourite! But as a software engineer I can't recommend it. So I'll plump for method 2.