ubercart

New module: uc_cano forked as uc_conditional_attributes

I have been using uc_cano on a few Ubercart sites and found that it is a great module for restricting attributes combinations when one attribute only makes sense if a certain option is selected on another attribute. Unfortunately, the module is very minimally maintained and there are many bugs present in the most recent release. Several community patches have shown up to fix some of the issues, but they need to be mixed and matched to get a working module. Furthermore, out of the box uc_cano is not compatible with the uc_node_checkout nor uc_aac modules.

In order to future centralize development efforts, facilitate releases and maintain a more up-to-date code base, I have collected the various community contributions plus written several fixes of my own and forked uc_cano as uc_conditional_attributes, now available on Drupal.org at http://drupal.org/project/uc_conditional_attributes. I hope you enjoy the new module! A dev release is available and I am hoping to add the first stable release soon after two known bugs are taken care of.

Some the changes I have made to cano include:

  • Rewrite of the JavaScript code and the addition of hooks
  • Addition of a new hooks & preliminary API for other modules to use
  • Compatibility with uc_aac (patch also required to uc_aac until it gets accepted upstream)
  • Compatibility with uc_node_checkout

Drupal module & patch work

I have been working closely with a company in the United States called Grindflow Management LLC (grindflow.com) on several Web projects including a few Drupal sites. I'm excited to say that over the holidays I had a chance to finally put together some of the patches and modules we worked on! More details on each below.

Patches

  • Add support for simultaneous display of multiple product nodes on one page. (#1377310)
    This patch for uc_option_images resolves the problem where uc_option_image would cease to function if multiple nodes were displayed on one page (for example, if promoted to the front page or if displayed in a View). The issue here was that drupal_add_js() in Drupal 6.x has an annoying bug (feature?) where array_merge_recursive() is used when the same JavaScript settings data is added multiple times. The code that generates the settings in uc_option_image (validly) has no idea if you're going to be displaying one node or ten, so each time the settings data for a node is generated is must be merged and the merging caused all sorts of weird duplicate data with invalid offsets. The workaround to the drupal_add-js() problem was to use a static array to ensure that the global settings only get added once, and to use string key indexes for the node settings data so that array_merge_recursive() would not attempt to merge them as numerical indexes.
  • UC Option Image doesn't work with required attributes (#1377310)
    This bug, also in uc_option_images, was due to the fact that the uc_option_image attempts to analyst the first attribute and preload the set option image for its default value if applicable. The problem with required attributes is that they don't have a default value, so an attempt to generate the default image would always fail. This failure meant that there was no HTML img element generated, so future calls to switch the option image also failed since there was no img element to operate on in the first place. Fixed by defaulting to the 'no image' if analyzing the first attribute fails.
  • Compatibility with Ubercart Ajax Attribute Calculations module (#1377310)
    Yet another one for uc_option_image, this patch adds uc_aac compatibility by changing uc_option_image's JavaScript code to work with Drupal Behaviours. Previously, the code would only run once upon page load so when uc_aac changed the DOM, uc_option_image would break. Using Drupal Behaviours also means paying attention to the 'context' variable, which also played a role in making multiple products on a single page work.
  • Some hooks and form handlers have not been updated for Drupal 6 (#1377310)
    The uc_invite_discount was originally written for 5.x and many portions were not updated for Drupal 6.x, leaving it rather broken. This patch fixed these issues and made it confirm to 6.x code style standards.
  • Preserve attributes from product form when redirecting anonymous users to node checkout form after logging in (#1376864)
    This bug in uc_node_checkout makes it so that anonymous users logging in get redirected back to the product configuration form with the attribute data preserved. Prior to this patch, users would get redirected to the node successfully but their selection of attributes was erased and they would have to start over.

Modules

  • Ubercart Attribute Per Role: http://drupal.org/project/uc_attribute_per_role
    This project allows site administrators to choose attributes that they would like hidden from display based on the user's active roles. For example, a 'gift wrap' attribute could be shown to retail (anonymous/authenticated) users but a definition could be added to hide this attribute for members of the 'wholesaler' role.

  • Clean Module List: http://drupal.org/project/clean_module_list
    This module is a very simple module that with the help from a bit of JavaScript, hides the dependency information (Depends on/Requires/Required by) in the module list and provides a controls to dynamically show/hide the information without a page reload. It can be used in conjunction with module_filter to provide an even cleaner and more searchable module list.
  • Ubercart Chained Attributes and Options (CANO): http://drupal.org/sandbox/firewing1/1374824
    This sandbox is the temporary home for the uc_cano project until I promote it to a full project. It is the continued development of the original module posted by Vizteck Solutions here. The code base is rather stagnant at the moment and I have a few bug fixes and feature improvements planned, so I am in touch with the original sponsor and developers of the module to ensure that all parties agree to move the new code to Drupal.org.
  • Ubercart Pay After Checkout: http://drupal.org/sandbox/firewing1/1229572
    I have written about this one before, uc_payafter allows users to pay for a product after checkout. It supports a configurable set of checkout panes just like the original checkout process does, so you can choose which panes appear during the initial checkout and during the payment (payafter) checkout processes. I am hoping to get the chance to refactor some of the code soon to reduce code duplication. Once that's done, I will promote it to a full project.

Ubercart modules: Manual shipping quote & payment after checkout

I have been working with a client to setup an Ubercart store customized to their needs and one of the things we came across as we launched the store is that because of the nature of the items being sold, it was very difficult to give a accurate shipping estimate. Thus, I set off to find a way to enable customer payments after checkout and to enter the shipping quotes manually.

These two modules, uc_manual_shipping and uc_payafter, are the fruit of these efforts. I hope you find them useful!

Note that this is my first release of these modules and the code should be considered a beta and work in progress. They are untested so far and not recommended for use on a production site just yet. All information about installation, configuration and further development is available in the README.txt file, DEVELOPERS.txt file and/or source code comments. If you have made some changes or improvements, please let me know in the comments! I would love to hear about your changes and I would be more than happy to apply any patches or bugfixes.

uc_manual_shipping enables the store administrators to manually enter shipping quotes on orders after a user has passed through checkout. It can be used in combination with uc_payafter to have users create go through regular checkout without paying, and then pay later once a shipping quote has been submitted by a store administrator.
Download uc_manual_shipping-6.x-1.0.tar.gz

uc_payafter duplicates the checkout process and allows users to perform payments on their orders after checkout at the URL cart/checkout/pay/$ORDER_ID. Store administrators can email users different invoice templates after payment.
Download uc_payafter-6.x-1.1.tar.gz

Update 2011-06-08: A user in the comments, Moises, has pointed out that in the 6.x-1.0 release of uc_payafter there was a typo that would prevent users from being able to select the request shipping invoices in the conditional actions configuration. I have updated uc_payafter below to fix this bug.

Update 2011-11-08: uc_payafter has a new home! I have created a Drupal Sandbox project for the code here. Once I have the chance to work on the code a bit more, I will promote it to full project status and update the download links in this post.

The sequel to my Ubercart i18n adventures

It has been a while since I last wrote about Ubercart, but I'm still working on some multi-lingual stores for clients. I have opted for disabling the stock Catalog module and using Views instead since Views is so much more flexible and easier to theme. I have a very simple setup; some terms in a vocabulary that is localized per-term, and then a custom View that takes a term name as an argument and returns nodes belonging to that term and displays them in a nicely themed grid.

Recently, I ran into an irritating issue where the View would return results from the wrong language if two languages had the same term name. After hours of investigating (and learning all about how to implement View handlers and plugins), it seems that the stock taxonomy term argument validator for Views cannot differentiate between terms of the same name in different languages. So if multiple languages contain the term "Stewart Adam" for example, the view will just returns nodes for whichever term (and therefore language) comes first in the database query. To be fair, the i18n module adds the "language" column to the term_data table so it's not really View's fault... Nonetheless, I was surprised that the i18n module had not already corrected this issue.

I've just reported Drupal issue #832100, Taxonomy term argument validator should not validate terms defined in other languages that includes a fix to the problem by limiting query for term names to terms within the active language. It's not the greatest way to go about solving it since it essentially just copies the original validator and makes two tiny modifications in the SQL query, but it's better then modifying the View module directly.

Translating or internationalizing an Ubercart store: Common problems & solutions

Although Drupal 6 handles node translation with the i18n module very well, Ubercart seems to have a bit of difficulty with it. I believe that most of the problems are due to the nature of translated nodes, which are actually completely different nodes that are glued together with some metadata. This fact, for example, is the reason Ubercart will create a product translation as a new product separate from the original.

As I tried to setup a multi-lingual store (English/French) store with Ubercart, I encountered many problems that seemed to be all over Google, but had no resolution posted or the solutions to the problems were fragmented across different sites and support threads. So after a lot of searching, reading PHP backtraces and hacking the Ubercart core modules, I was finally able to have a good Ubercart catalog set up in two languages! I've compiled a list of problems and their solutions below if you are also having problems. Feel free to use any part or all of the code samples on your site.

Note: The code in this tutorial has been confirmed to work with Ubercart 2.2. It should also work with future versions, but may require some minor tweaks.

Problem: "add to cart" and other form buttons need to be translated

Ubercart handles form button texts like "add to cart" via multi-lingual variables. Multi-lingual variables differ from strings that use Drupal's t() function in that they will not appear in the site's Translate Interface utility. Instead, multi-lingual store a value for each of the site's languages and load the appropriate string depending on which language the user is currently browsing in.

In the case of "add to cart", this simply means that one must browse to Administer > Store Administration > Configuration > Product Settings and click on the Edit button. Switch the site language to the desired language and enter new values in the "Add to cart button text" configuration box. Once the values have been saved, you will be able to switch to a different language and enter new values for that language.

Problem: Ubercart does not synchronize product data across translated nodes (product information needs to be updated for each translation)

As I mentioned in the introduction, this is due to the fact that each translation is in fact a node to itself. Since Drupal assigns each node with content type "product" the standard Ubercart attribute set, each product node must be updated individually. Fortunately, there is a quick and easy was to solve this by creating a custom module:

  1. Enable the i18nsync ("Synchronize Translations") module in Administer > Site building > Modules
  2. Create a custom module which will hook into i18nsync:
    1. Create the folder sites/all/modules/custommod
    2. Paste the following into sites/all/modules/custommod/custommod.info:
      name = Custom module
      description = A custom module which helps with the translation of an Ubercart store
      core = 6.x
    3. Paste into sites/all/modules/custommod/custommod.module:
      <?php
      /**
        * Implementation of hook_i18nsync_fields_alter().
        */
      function custommod_i18nsync_fields_alter($fields, $type) {
        if(
      in_array($type, uc_product_types())) {
         
      $fields['uc_products']['#title'] = 'Products';
         
      // These values were found by doing a print_r($node) in node-product.tpl.php
         
      $fields['uc_products']['#options'] = array (
           
      'model' => 'SKU',
           
      'list_price' => 'List Price',
           
      'cost' => 'Cost',
           
      'sell_price' => 'Sell Price',
           
      'weight' => 'Weight',
           
      'weight_units' => 'Weight Units',
           
      'dim_length' => 'Length',
           
      'dim_width' => 'Width',
           
      'dim_height' => 'Height',
           
      'length_units' => 'Length Units',
           
      'pkg_qty' => 'Quantity',
           
      'default_qty' => 'Default quantity to add to cart'
         
      );
        }
      }
      ?>

    (Thanks to these two commenters for posting code updates)

  3. Go to Administer > Site building > Modules and enable "Custom module"
  4. Go to Administer > Content management > Content types and edit the "Product" content type
    1. Under Workflow Settings > Synchronize translations, select all fields you would like to synchronize. However, do not select the Taxonomy terms option (see below for why)
    2. Repeat this step for any other product classes/content types whose product data you want synchronized

That's all! If you edit a node in one language, you should see the updated product data in all other languages as well. The only caveat is that product attributes and product options will still need to be added and updated on each translation.

Thanks to user gupa on Ubercart support forums for posting this fix (see reference 1)

Problem: Taxonomy terms are untranslated in Catalog block

This one had me puzzled for a long time. At first, I had enabled localized terms for the Catalog vocabulary. After I created some terms, I translated them using the Translate Interface tool as usual, but this did not work. Editing a product would show the localized term in the Catalog list, however viewing the product always resulted in the untranslated term being displayed on the Catalog block. The solution is to navigate to Administer > Content management > Taxonomy and edit the Catalog vocabulary, choosing Per language terms. Use the following procedure to define taxonomy term translations:

  1. Browse to Administer > Content management > Taxonomy
  2. Add a new term with its translations:
    1. Add a new term to the Catalog vocabulary in the default language of your store and choose that language from the drop-down menu
    2. Repeat the above step, but enter the desired translated texts for the term (including a translated term name)
    3. Repeat the above step, but enter the desired translated texts for the term (including a translated term name)
  3. Click on the Translation tab of the taxonomy interface
  4. Click Create new translation and pair up the original term with its translations
  5. Repeat the step above for each term that you created earlier.

This is why you do not want to synchronize taxonomy terms, with this setup each term is translated the same way nodes are - separate terms per node, linked together with some metadata. Synchronizing taxonomy terms would result in your products appearing in the right categories but in the wrong languages!

Problem: Adding items to the cart in one language, then switching to another language does not localize the cart contents

This is an unusual situation, but it's a very annoying problem; adding an item to the cart, switching languages and then click on that same item in the cart will open the product in first language, not the one the customer is browsing your website in. To solve this, hook_cart_item() will be used to override the default cart rendering with our own version which will replace node with translated nodes. Open the custom module file, sites/all/modules/custommod/custommod.module, which was created earlier and add before the closing ?> tag:

<?php
/**
  * Implementation of hook_add_to_cart().
  */
function custommod_add_to_cart($nid, $qty, $data) {
 
/* Due to Drupal's use of multiple nodes for product translations, the same
   * product in a different language is treated as a different product entirely.
   * This is problematic as the same product in different languages can be added
   * to the cart simultaneously. This function works around that problem by
   * always using the tnid/original node. As a result, the cart must be
   * localized as it is displayed.
   */
 
$node = node_load($nid);
 
// Determine if this node is the source node or a translated one
  // Remember: tnid is 0 if there are no translations
 
$is_source = ($node->nid == $node->tnid || $node->tnid == 0) ? 1 : 0;
  if (
$is_source) {
   
// If it is the source, then all is well…
   
$result[] =  array('success' => TRUE);
  } else {
   
/* If we are not the source node, then fail to add this product silently and
     * call uc_cart_add_item() to add the source node's product instead. It will
     * be localized later - see custommod_cart_item()
     */
   
uc_cart_add_item($node->tnid, $qty, $data);
   
$result[] = array('success' => FALSE, 'silent' => TRUE);
  }
 
// Remember: We need an array in an array here
 
return $result;
}

/**
  * Implementation of hook_cart_item().
  */
function custommod_cart_item($op, &$item) {
 
/* hook_cart_display() isn't really a hook, it's mostly for internal use.
   * However, we do need to access later. Setting $item->module forces
   * a module_invoke() call in uc_cart.module to call custommod_cart_display()
   * instead of the default uc_product_cart_display(). We will call
   * uc_product_cart_display() inside our function to ensure things work as
   * usual in future versions.
   */
 
$item->module = "custommod";
 
/* Note that although it is possible to use check for case 'load' in $op and
   * then override the $item->nid and $item->title values, this will cause bugs
   * when attempting to add or remove products in different languages. To
   * resolve these bugs, we are forcing the use of custommod_cart_display() and
   * rewriting the code for the title, img, and anchors to localize the cart.
   */
}
/**
  * Implementation of hook_cart_display().
  */
function custommod_cart_display($item) {
 
/* Call uc_product_cart_display() to get things setup as usual and to ensure
   * this hack still works even if uc_product_cart_display changes at some point
   * in the future.
   */
 
$display_item = uc_product_cart_display($item);
 
// Get the translations, if any.
 
$node = node_load($item->nid);
  global
$language;
 
$translations = translation_node_get_translations($node->tnid);
  if (
$translations[$language->language]) {
   
// Reminder: NEVER override the nid. That is what causes the bugs!
   
$tnode = node_load($translations[$language->language]->nid);
   
$display_item["title"]["#value"] = node_access('view', $tnode) ? l($tnode->title, 'node/'. $tnode->nid) : check_plain($tnode->title);
   
$display_item["image"]["#value"] = uc_product_get_picture($tnode->nid, 'cart');
  }
  return
$display_item;
}
?>

After reloading the Custom module at Administer > Site building > Modules, the cart should behave properly when switching languages. As well, adding a product to the cart in one language, switching languages, then adding it again should not result in two different products being added to the cart. Instead, the quantity of the product will increase by 1.

Credit for this solution goes to user Docc at Ubercart forums, who posted the code sample (see reference 3)

Problem: Ubercart does not localize products during checkout

This one was a bit trickier to solve, since there's no real elegant way to trick ubercart into localizing the products while using the standard checkout pane. As a result, we will have to disable the stock checkout pane and use the replacement provided by the code below instead (the replacement pane is called "Your order").

<?php
/**
  * Implementation of hook_checkout_pane().
  */
function custommod_checkout_pane() {
 
/* Replacement for standard cart contents pane. Although hook_cart_item() can
   * be used to localize the checkout pane, then we get into trouble while
   * trying to localize the cart display (see the comments above). The best way
   * that I can think of to work around this is to disable the stock cart
   * contents pane and enable this one instead.
   */
 
$panes[] = array(
   
'id' => 'custommod_cart',
   
'callback' => 'custommod_checkout_pane_custommod_cart',
   
'title' => t('Your order'),
   
'desc' => t('Display the (localized) contents of a customer\'s shopping cart.'),
   
'weight' => 2,
   
'process' => TRUE,
   
'collapsible' => FALSE,
  );
  return
$panes;
}

/**
  * Callback for our implementation of hook_checkout_pane()
  */
function custommod_checkout_pane_custommod_cart($op, &$arg1, $arg2) {
 
/* The code below is copied from uc_checkout_pane_cart() and is slightly
   * modified to localize the cart contents before displaying it. If you are
   * using this, you need to keep an eye on uc_checkout_pane_cart() to make sure
   * that if there is an important change or bugfix, you make the same change
   * here.
   */
 
switch ($op) {
    case
'view':
     
$contents['cart_review_table'] = array(
       
'#value' => theme('cart_review_table'),
       
'#weight' => variable_get('uc_pane_cart_field_cart_weight', 2),
      );
      return array(
'contents' => $contents, 'next-button' => FALSE);
    case
'review':
     
$items = uc_cart_get_contents();
     
$output = '<table>';
     
$context = array(
       
'revision' => 'themed',
       
'type' => 'cart_item',
       
'subject' => array(),
      );
      global
$language;
      foreach (
$items as $item) {
       
$node = node_load($item->nid);
       
$translations = translation_node_get_translations($node->tnid);
        if (
$translations[$language->language]) {
         
$tnode = node_load($translations[$language->language]->nid);
        } else {
         
$tnode = $node;
        }
       
$desc = check_plain($tnode->title) . uc_product_get_description($item);
       
$price_info = array(
         
'price' => $item->price,
         
'qty' => $item->qty,
        );
       
$context['subject'] = array(
         
'cart' => $items,
         
'cart_item' => $item,
         
'node' => $tnode,
        );
       
$output .= '<tr valign="top"><td>'. $item->qty .'&times;</td><td width="100%">'. $desc
                 
.'</td><td nowrap="nowrap">'. uc_price($price_info, $context) .'</td></tr>';
      }
     
$output .= '</table>';
     
$review[] = $output;
      return
$review;
  }
  return
$result;
}
?>

Thanks to user totsubo at Ubercart forums for helping me test this part of the module!

See also

References:

  1. Information about localized taxonomy in thread Multiple language Ubercart website on the Ubercart support forums
  2. Lots of good information at a thread titled i18n issues i D6/UC2 for Multilingual sites on the Ubercart support forums
  3. Method for product synchronization was based on a small code sample by Docc at Drupal node #456358