Drupal Commerce (commerce.module) is a great extension to Drupal CMS (drupal.module) for full functionaly eCommerce online shop functionality. Less for a standard products shop (where other more specialized eCommerce systems like OXID, Shopware, Magento or others come in), but expecially for very custom shops with for example user-customizable products.
We've already implemented several custom product shops for our German customers. In some cases we also needed a functionality to create orders from code programatically.
That's useful for example, if you import old orders, already have a list of predefined customer orders and other usecases.
It was always a horror to find out the right way to implement this, because it's not fully described by examples or documentation yet. There are many things you can do wrong and still I'm not sure if there are people who will help me to make the process even better...
So I decided to provide my implementation to you to make it easier for others to implement an order generation from code. The following code shows all steps from an existing user (that's what I assume) to the full order containing addresses, a payment method and that are ready for final checkout.
The only thing that's not yet included is the shipping method, because I've just had to handle digital products with no shipping. Feel free to add an implemantation for that.
I hope it will help you and please provide some feedback or even better implementations for your order generation.
Talked enough, here's the code:
/**
* Creates an order for the given user containing the product with the given id.
* This example may be extended easily to create an order containing several
* products, a shipping and so far.
*
* Returns a commerce order that's now saved in the database with the status
* "processing and can be finished by an administrator in the UI.
* Throws an exception, if something is wrong.
*
* @param integer $uid The numeric user id, e.g. "1"
* @param integer $product_id The numeric product id (NOT SKU! ;))
* @return CommerceORder
* @throws Exception
*/
function my_module_api_create_order($uid, $product_id) {
if (empty($uid) || !is_numeric($uid)) {
throw new Exception('User ID may not be empty and must be numeric!');
}
$user = user_load($uid);
if (empty($product_id) || !is_numeric($product_id)) {
throw new Exception('Product ID may not be empty and must be numeric!');
}
// Use a transaction to ensure that no broken order is created!
$transaction = db_transaction();
try {
// Create the new order in checkout; you might also check first to
// see if your user already has an order to use instead of a new one.
$order = commerce_order_new($user->uid, 'checkout_checkout');
// Save the order to get its ID.
commerce_order_save($order);
// Get the order wrapper which provides helper functionality
$order_wrapper = entity_metadata_wrapper('commerce_order', $order);
// Add address profiles.
// IMPORTANT! The address MUST be set before the line item to
// correctly determine the country of supply.
// IMPORTANT2: This address setup is made based on single fields in the user
// account. If you already use an address field in the user account
// this is much simpler and you should have a look at the
// commerce_extra module for address fields ;)
// Billing
// Check that we have at least all REQUIRED fields!
if (empty($user->field_firstname[LANGUAGE_NONE][0]['value'])) {
throw new Exception('field_firstname may not be empty!');
}
if (empty($user->field_lastname[LANGUAGE_NONE][0]['value'])) {
throw new Exception('field_lastname may not be empty!');
}
if (empty($user->field_city[LANGUAGE_NONE][0]['value'])) {
throw new Exception('field_city may not be empty!');
}
if (empty($user->field_zip[LANGUAGE_NONE][0]['value'])) {
throw new Exception('field_zip may not be empty!');
}
if (empty($user->field_street[LANGUAGE_NONE][0]['value'])) {
throw new Exception('field_street may not be empty!');
}
if (empty($user->field_country[LANGUAGE_NONE][0]['value'])) {
throw new Exception('field_country may not be empty!');
}
// We have to get or create the first order profile of the users to set the values into.
// We use a helper function for that!
$billing = _my_module_api_create_order_get_first_customer_profile('billing', $user);
$customer_profile_billing_array = array(
'country' => isset($user->field_country[LANGUAGE_NONE][0]['value']) ? $user->field_country[LANGUAGE_NONE][0]['value'] : NULL, // DE
'name_line' => NULL, // Testperson
'first_name' => isset($user->field_firstname[LANGUAGE_NONE][0]['value']) ? $user->field_firstname[LANGUAGE_NONE][0]['value'] : NULL, // Max
'last_name' => isset($user->field_lastname[LANGUAGE_NONE][0]['value']) ? $user->field_lastname[LANGUAGE_NONE][0]['value'] : NULL, // Mustermann
'organisation_name' => isset($user->field_firma[LANGUAGE_NONE][0]['value']) ? $user->field_firma[LANGUAGE_NONE][0]['value'] : NULL, // Muster AG
'administrative_area' => NULL,
'sub_administrative_area' => NULL,
'locality' => isset($user->field_wohnort[LANGUAGE_NONE][0]['value']) ? $user->field_wohnort[LANGUAGE_NONE][0]['value'] : NULL, // Musterstadt
'dependent_locality' => NULL,
'postal_code' => isset($user->field_zip[LANGUAGE_NONE][0]['value']) ? $user->field_zip[LANGUAGE_NONE][0]['value'] : NULL, // 32457
'thoroughfare' => isset($user->field_street[LANGUAGE_NONE][0]['value']) ? $user->field_street[LANGUAGE_NONE][0]['value'] : NULL, // Musterstraße 10
'premise' => NULL,
'sub_premise' => NULL,
'data' => NULL,
);
// Set the values in the profile entity and attache it to the order
$billing->commerce_customer_address[LANGUAGE_NONE][0] = $customer_profile_billing_array;
commerce_customer_profile_save($billing);
$order->commerce_customer_billing[LANGUAGE_NONE][0]['profile_id'] = $billing->profile_id;
// This is only required if you use the commerce_vat module and you want to
// set the vat id programmatically with implications on the price calculation!
if (!empty($user->field_vat_number[LANGUAGE_NONE][0]['value'])) {
$eu_vat_rc = _my_module_api_create_order_get_first_customer_profile('eu_vat_rc', $user);
$customer_profile_vat_array = array(
'value' => !empty($user->field_vat_number[LANGUAGE_NONE][0]['value']) ? $user->field_vat_number[LANGUAGE_NONE][0]['value'] : NULL,
);
$eu_vat_rc->commerce_vat_number[LANGUAGE_NONE][0] = $customer_profile_vat_array;
commerce_customer_profile_save($eu_vat_rc);
$order->commerce_customer_eu_vat_rc[LANGUAGE_NONE][0]['profile_id'] = $eu_vat_rc->profile_id;
}
// IMPORTANT: If we also use shipping address, this would be the
// place to set it here like we already did in "billing"!
// Save the order again to save the address attached.
$order_wrapper->save();
// Load whatever product represents the item the customer will be
// paying for and create a line item for it.
$product = commerce_product_load($product_id);
// Create a new line item with multiplicity = 1
$line_item = commerce_product_line_item_new($product, 1, $order->order_id);
// You may set line item properties here.
// Calculate the sell price!
rules_invoke_event('commerce_product_calculate_sell_price', $line_item);
// Save the line item
commerce_line_item_save($line_item);
// Attach the line item to the order using the order wrapper.
$order_wrapper->commerce_line_items[] = $line_item;
$order_wrapper->save();
// Now calculate the total price and save the order again.
commerce_order_calculate_total($order);
$order_wrapper->save();
// To create the payment we need some information from the order:
$total = $order_wrapper->commerce_order_total->amount->value();
$currency_code = $order_wrapper->commerce_order_total->currency_code->value();
$charge = array(
'amount' => $total,
'currency_code' => $currency_code,
);
// The following is an example for payment methods commerce_directdebit and bank_transfer.
// You may use this as example for other payment types!
// SEPA if we have the required users direct debit fields, otherwise bank transfer!
if (!empty($user->field_accountholder[LANGUAGE_NONE][0]['value']) && !empty($user->field_bic[LANGUAGE_NONE][0]['value']) && !empty($user->field_iban[LANGUAGE_NONE][0]['value'])) {
// SEPA
$payment_method = array(
'instance_id' => 'commerce_directdebit|commerce_payment_commerce_directdebit',
);
commerce_directdebit_transaction($payment_method, $order, $charge, $user->field_accountholder[LANGUAGE_NONE][0]['value'], '', '', 1, $user->field_bic[LANGUAGE_NONE][0]['value'], $user->field_iban[LANGUAGE_NONE][0]['value']);
}
else {
// BANK TRANSFER
$payment_method = array(
'instance_id' => 'bank_transfer|commerce_payment_bank_transfer',
);
commerce_bank_transfer_transaction($payment_method, $order, $charge);
}
// Set the payment id in the order array to make it available for invoicing
// (no idea why we have to do that manually here. Perhaps there's a better way?)
$order->data['payment_method'] = $payment_method['instance_id'];
$order_wrapper->save();
// Update the status to processing to allow manual finishing later on.
commerce_order_status_update($order, 'processing');
// No explicit transaction commit wanted by drupal...
} catch (Exception $e) {
// Something went wrong! We don't want to save a broken order to the database
// at all. So let's roll back the transaction:
$transaction->rollback();
// Add some information to the exception for easier debugging in an outer try / catch!
$e->user_id = $uid;
$e->product_id = $product_id;
// We've done what we could. Re-throw the exception and let an outer
// try/catch handle it for error logging.
throw $e;
}
// Yeah, the order was created successfully. Provide it to the outer call! :)
return $order;
}
/**
* Helper function to get the users already existing (as implemented: first)
* commerce customer profile and return it.
* If the user has no customer profile yet, create a new one and return that.
*
* @param string $type The type of the customer profile e.g. "billing".
* @param stdClass $user The user object.
* @return stdClass The CommerceCustomerProfile object.
*/
function _my_module_api_create_order_get_first_customer_profile($type, stdClass $user) {
$query = new EntityFieldQuery();
$query->entityCondition('entity_type', 'commerce_customer_profile')
->propertyCondition('uid', $user->uid)
->propertyCondition('type', $type);
$results = $query->execute();
if (!empty($results['commerce_customer_profile'])) {
// Profile already exists. Load!
$profile_info = reset($results['commerce_customer_profile']);
return commerce_customer_profile_load($profile_info->profile_id);
}
else {
// No profile yet. Create one!
return commerce_customer_profile_new($type, $user->uid);
}
}
?>
Finally some links that helped me to find out, how it works:
- https://drupal.org/project/commerce_examples
- http://www.phase2technology.com/blog/an-introduction-to-drupal-commerce…
- http://commerceguys.com/blog/creating-orders-drupal-commerce-api
Legal note: The provided code is not intended for production sites! This is just a development example. No liability, no warranties!