Programmatically create Ubercart products with attributes and selected options in Drupal 6

I have been working on a import script to import products from a previous custom Drupal module in to Ubercart. For this I have been using the Batch API to handle the large amounts of data. Upon trying to import some products using drupal_execute(), I came to realise that drupal_execute() can not be run in during a batch operation using Batch API.

So I ended up creating the node using node_save() which created the basic product.

<?php
function my_module_create_product($data) {
  require_once 
'modules/node/node.pages.inc'// Required for node_object_prepare();
  
$node = new stdClass();
  
$node->type 'product';

  node_object_prepare($node); // This sets up all the default node fields so we don't accidentally leave something off.

  // Copy over all the existing settings from Drupal 5.
  $node->uid 1;
  
$node->status $data->status;
  
$node->title $data->title;
  
$node->created $data->created;
  
$node->changed $data->changed;

  // Set Ubercart variables
  
$node->model $data->model_number// the SKU is a required field, so I generated a SKU based on the node title
  
$node->list_price $data->price;
  
$node->cost $data->price;
  
$node->sell_price $data->price;
  
$node->default_qty 1;
  
$node->pkg_qty 1;

  // Set taxonomy + menu etc if you need to
  
$node->taxonomy = array();
  
$node->menu = array();

  // Save the node
  
node_save($node);  // This will update the $node object with the $node->nid which is important for the next step.
}
?>

We now have the node created with all the required information. From here, you would usually set up a the $form_state['values'] array and pass on to uc_object_attributes_form() to add the attributes to the form followed by uc_object_options_form() to set the options for these attributes both using drupal_execute(). But as we can't do this, I copied the code from the submit handlers for both of these forms and merged them in to one (there are a few queries which are written by uc_object_attributes_form_submit() which are then overwritten or delete by uc_object_options_form_submt() which we can leave out or alter slightly to make the code shorter and to do less database queries. I have added some comments to the code which shows you the process.

<?php
function my_module_create_product($data) {
  
// $node creation stuff from above
  
  // To set up the attributes we need to create a keyed array where the key is the attribute ID which contains an array of the options which need to be set.  The option in the keyed array will be set as the default option for that attribute.  If the attribute is just a blank textfield and doesn't have any options, then just pass an empty array.
  
$attributes = array(
    
=> array(12345678), // This could be a list of T-shirt colours, so which ever colour option 1 is will be the default option.
    
=> array(910111213), // This could be T-shirt sizes, again the first number will be the default which in this case is option 9.
  
);

  // Go through all our attributes that we have set and set up the options in the database.
  
foreach ($attributes as $aid => $options) {
    
// Load the attribute
    
$attribute uc_attribute_load($aid);
    
// Set the default to be the first option.
    
$oid $attributes[$aid][0];
    
// Add the attribute to for the node and set the default option
    
db_query("INSERT INTO {uc_product_attributes} (nid, aid, label, ordering, default_option, required, display) SELECT %d, aid, label, ordering, %d, required, display FROM {uc_attributes} WHERE aid = %d"$node->nid$oid$aid);
    
// Set the options if they are provided
    
if (is_array($attribute->options) && count($attributes[$aid]) > 0) {
      
// Cycle through all the available options (the $option object here contains all necessary values to be put in the database.
      
foreach ($attribute->options as $option) {
        if (
in_array($option->oid$attributes[$aid])) { // Check to see if the current option is in our keyed array.
          
db_query("INSERT INTO {uc_product_options} (nid, oid, cost, price, weight, ordering) VALUES (%d, %d, %f, %f, %f, %d)"$node->nid$option->oid$option->cost$option->price$option->weight$option->ordering);
        }
      }
    }
  }
}
?>

We won't be using adjustments, so I haven't added that to my code. If you wish to further your code with adjustments you will need to find the necessary form and submit function for it.

Comments

[...] This post was mentioned on Twitter by Drupal Planet, SanjeevJain and Drupal Updates, James Tombs. James Tombs said: Programmatically create Ubercart products with attributes and selected options in #Drupal 6 http://goo.gl/j4Ss [...]

Great post! I've been writing a lot of Ubercart import and export logic, reading and writing in Excel CSV format to facilitate repeated product modifications, and large store inventories. I'm interested in sharing code and discussion with you.

For example, I'm currently wondering if I need to back up and redesign a product line's import & export logic because UC does not appear to handle product adjustments in a manner I thought it could. I thought product adjustments could be treated like 'pizza toppings' where multiple options could be added to a product and each option's price would just add to that product's total. I thought the final sku purchased would be the default sku, followed by each adjustment, one after another. However, UC appears to want to only present a single SKU for the purchase: the UI enabling a customer to pick multiple options at once (checkboxes) deactivates that product's ability to have product adjustments, and when using multiple attributes with product adjustments one is expected to provide a product adjustment for each combination of attribute possible.

Anyway, I'm working with that. If you're interested in chatting, you've got my email...

I have just finished a similar project, only my one takes data on a daily basis from a tired old dBase data file coming from a DOS accounting application (yeah, I know!). The data files I grabbed from this are to be programmatically morphed into useful product data in Ubercart. All of that required a bootstrapped Drupal.

I found that although node_save worked, I had to use drupal_execute in order to get the CCK fields (field_image_cache - the images for the item, and url - one I created) to save properly and stay there. Node_save did some odd stuff like removing files from the files table without being asked to. Don't know why...too focussed on getting the job done.

drupal_execute() is the right way to do it and the way that I would have done it, had I not been using the Batch API. This particular import was only 300 or so nodes but the Batch API makes everything a little easier when it comes to importing all the data prior to launching the site.

The good thing with the import for me was there were no images or files of any sort which needed to be saved which made node_save() a lot easier. CCK fields are fine (with perhaps the exception of image/file fields but I didn't have any).

The options work as I would expect to be honest with the SKUs using the default unless otherwise specified.

In the case of Tshirts, if you had the following:

Red | Small
Red | Large
Blue | Small
Blue | Large

There are 4 different options to chose from. From that you can define a SKU per option or one SKU for the lot, or one SKU for blue and another for red.

There is one issue I am having at the moment with node_checkout, which is showing all options on the node checkout form when it should only be showing a couple of options.

I know this seems like a quibble, but could you rewrap your code at 72 characters per line (or similar)? This article is messing with my RSS reader, and I'm guessing I'm not alone. Thanks!

Feel free to delete this comment when read.

If you would like to make working with attributes easier, then please help by testing this patch for ubercart:
http://drupal.org/node/488422

Sorry about that, I forgot the teaser/more tag, I always try to avoid code appearing in RSS.

I'm new to drupal coding, and have enjoyed working with the API. I've now been given a task of importing legacy data into ubercart format, including import to term fields. It's all got to be done manually, I suppose in part due to image requirements, as well as the term fields, and now possibly the need for custom ubercart attribute and option properties (e.g.: store legacy attribute ID as a varchar).

Can anyone advise as to the storage of ubercart attribute and option primary keys as varchar for legacy data, but auto-incrementing integer following that? The autoincremented integers could be a custom format (e.g.: new-1001, new-1002, etc).

Thanks, in advance.

I don't think you will be able to. Best thing to do would be to create a lookup table for the attribute ids. Then hopefully you will be able to use ubercart hooks to use the table for reports.

thank you so mach

Why not use uc_cart_add_item() instead of the DB call?

I didn't know it existed.

Because that doesn't have anything to do with what he's doing?