Nov 30 2016
Nov 30

Freitag logoOur latest site with Drupal Commerce 1.x went live in July 2016. It is Freitag. Since then we’ve been adding several new commerce related features. I feel it’s time to write a wrap-up. The site has several interesting solutions, this article will focus on commerce.

First a few words about the architecture. platform.sh hosts the site. The stack is Linux + nginx +  MySQL + PHP, the CMS is Drupal 7. Fastly caches http responses for anonymous users and also for authenticated users having no additional role (that is, logged-in customers). Authcache module takes care of lazy-loading the personalized parts (like the user menu and the shopping cart). Freitag has an ERP system to which we connect using the OCI8 PHP library. We write Behat and simpletest tests for QA.

We use the highly flexible Drupal Commerce suite. 23 of the enabled Freitag contrib modules have a name starting with ‘commerce’. We applied around 45 patches on them. Most of the patches are authored by us and 15 of them have already been committed. Even with this commitment to solve everything we could in an open-source way we wrote 30.000+ lines of commerce-related custom code. Still, in March 2016 Freitag was the 3rd largest Drupal customer contributor.

The words ‘product’ and ‘product variation’  I’ll be using throughout the article correspond to ‘product display node’ and ‘product’ in Drupal Commerce lingo.

ERP

ERP is the source of all products and product variations. We import this data into Drupal on a regular basis using Feeds. (Now I would use Migrate instead, it’s better supported and easier to maintain.) ERP also lets Drupal know about order status changes, sends the shipping tracking information and informs Drupal about products sent back to Freitag by customers.

There is data flowing in the opposite direction as well. ERP needs to know about all Drupal orders. Also, we create coupons in Drupal and send them to ERP too for accounting and other reasons.

Emails

We send commerce-related emails using the Message stack. This way we can have order-related tokens in our mails and we can manage and translate them outside the Rules UI. Mandrill takes care of the mail delivery.

Payment gateway

It was a client requirement to use the Swiss Datatrans payment gateway. However, at the time of starting  the project, Commerce Datatrans (the connector module on drupal.org) was in dev state and lacked several features we needed. Pressed for time we opted for buying a Datatrans Drupal module from a company offering this solution. It turned out to be a bad choice. When we discovered that the purchased module still does not cover all our needs and looked at the code we found that it was obfuscated and pretty much impossible to change. Also, the module could be used only on one site instance which made it impossible to use it on our staging sites.

We ended up submitting patches to the Commerce Datatrans module hosted on drupal.org. The module maintainer, Sascha Grossenbacher (the well-known Drupal 8 core contributor) helped us solving several issues and feature requests by reviewing our patches. This process has lead to a stable release of Commerce Datatrans with a dozen of feature improvements and bugfixes.

Additional to Datatrans we use Commerce Custom Offline Payments to enable offline store purchases by store staff and bank transfer payments.

Currencies

The site works with 7 different currencies, some of them having two different prices depending on the shipping country. Prices come from ERP and we store them in a field collection field on the product. We do not use the commerce_price field on the product variation.

Tax

Freitag ships to countries all around the world. VAT calculations are performed for EU, Switzerland, UK, Japan, South Korea and Singapore. To implement this functionality our choice fell on the commerce_vat module. Adding commerce_eu_vat and commerce_ch_vat released us from having to maintain VAT rates for EU and Switzerland ourselves. For the 3 Asian countries we implemented our own hook_commerce_vat_rate_info().

We have two different VAT rates for most of the countries. This is because usually a lower VAT rate applies to books. Drupal imports the appropriate VAT rate from the ERP with the product variation data. This information is handled by price calculation rules in Drupal.

Shipping

Freitag delivers its products using several shipping providers (like UPS, Swiss Post) all around the world. Most shipping providers have many shipping rates depending on the destination country, speed and shipped quantity (weight or volume). On checkout the customer can choose from a list of shipping services. This list needs to be compatible with the order.

We used rules to implement the shipping services in Drupal based on the Commerce Flat Rate module. For this end we trained our client to set up and maintain these rules themselves. It was not easy: shipping rules are daunting even for experienced commerce developers. First we needed to set up the “Profile Address” rules components. Then we configured the “Place of Supply” components. We applied these in turn in the condition part of the shipping rules components themselves.

The weakest point of any  implementation based on Rules is the maintenance. It’s not easy to find a specific rule after you created it. Having 250 rules components for only shipping made this feeling stronger.

The shipping line item receives the VAT rate of the product with the highest VAT rate in the order.

Coupons

Freitag has 6 different coupon types. They differ in who can create them, who and where (online/offline) can redeem them, whether partial redemption is possible, whether it’s a fixed amount or percentage discount and whether Freitag accounting needs to know about them or not.

Based on these criteria we came up with a solution featuring Commerce Coupon. Coupons can be discount coupons or giftcards. Giftcard coupons can only have a fixed value. Discount based coupons can also apply a percentage discount. The main difference between them is that customers can partially redeem giftcards, while discount-based coupons are for one-time use.

To make coupons work with VAT was quite tricky. (To make things simpler we only allowed one coupon per order.) Some coupon types work as money which means that from an accounting point of view they do not actually decrease the order total (and thus the VAT) but work as a payment method. Other coupon types however do decrease the order total (and thus the VAT). At the same time Drupal handles all coupons as line items with a negative price and the Drupal order total does decrease in either case.

The solution we found was to use Commerce proportional VATAxel Rutz maintains this brilliant little module and he does this in a very helpful and responsive manner. All the module does is adding negative VAT price components to coupon line items to account for VAT decrease. It decreases the order total VAT amounts correctly even if we have several different VAT rates inside the order.

Conclusion

Although there’s always room for increasing the complexity of the commerce part of the site (let’s find some use case for recurring payments!), it’s already the most complicated commerce site I’ve worked on.  For this Drupal Commerce provided a solid foundation that is pleasant to work with. In the end, Drupal enabled us to deliver a system that tightly integrates content and commerce.

I also would like to say thanks to Bojan Živanović from Commerce Guys who provided me with valuable insights on the legal aspects of tax calculation.

Oct 24 2016
Oct 24

In this blog post I will present how, in a recent e-Commerce project built on top of Drupal7 (the former version of the Drupal CMS), we make Drupal7, SearchAPI and Commerce play together to efficiently retrieve grouped results from Solr in SearchAPI, with no indexed data duplication.

We used the SearchAPI and the FacetAPI modules to build a search index for products, so far so good: available products and product-variations can be searched and filtered also by using a set of pre-defined facets. In a subsequent request, a new need arose from our project owner: provide a list of products where the results should include, in addition to the product details, a picture of one of the available product variations, while keep the ability to apply facets on products for the listing. Furthermore, the product variation picture displayed in the list must also match the filter applied by the user: this with the aim of not confusing users, and to provide a better user experience.

An example use case here is simple: allow users to get the list of available products and be able to filter them by the color/size/etc field of the available product variations, while displaying a picture of the available variations, and not a sample picture.

For the sake of simplicity and consistency with Drupal’s Commerce module terminology, I will use the term “Product” to refer to any product-variation, while the term “Model” will be used to refer to a product.

Solr Result Grouping

We decided to use Solr (the well-known, fast and efficient search engine built on top of the Apache Lucene library) as the backend of the eCommerce platform: the reason lies not only in its full-text search features, but also in the possibility to build a fast retrieval system for the huge number of products we were expecting to be available online.

To solve the request about the display of product models, facets and available products, I intended to use the feature offered by Solr called Result-Grouping as it seemed to be suitable for our case: Solr is able to return just a subset of results by grouping them given an “single value” field (previously indexed, of course). The Facets can then be configured to be computed from: the grouped set of results, the ungrouped items or just from the first result of each group.

Such handy feature of Solr can be used in combination with the SearchAPI module by installing the SearchAPI Grouping module. The module allows to return results grouped by a single-valued field, while keeping the building process of the facets on all the results matched by the query, this behavior is configurable.

That allowed us to:

  • group the available products by the referenced model and return just one model;
  • compute the attribute’s facets on the entire collection of available products;
  • reuse the data in the product index for multiple views based on different grouping settings.

Result Grouping in SearchAPI

Due to some limitations of the SearchAPI module and its query building components, such plan was not doable with the current configuration as it would require us to create a copy of the product index just to apply the specific Result Grouping feature for each view.

The reason is that the features implemented by the SearchAPI Grouping are implemented on top of the “Alterations and Processors” functions of SearchAPI. Those are a set of specific functions that can be configured and invoked both at indexing-time and at querying-time by the SearchAPI module. In particular Alterations allows to programmatically alter the contents sent to the underlying index, while the Processors code is executed when a search query is built, executed and the results returned.
Those functions can be defined and configured only per-index.

As visible in the following picture, the SearchAPI Grouping module configuration could be done solely in the Index configuration, but not per-query.

SearchAPI: processor settings

Image 1: SearchAPI configuration for the Grouping Processor.

As the SearchAPI Grouping module is implemented as a SearchAPI Processor (as it needs to be able to alter the query sent to Solr and to handle the returned results), it would force us to create a new index for each different configuration of the result grouping.

Such limitation requires to introduce a lot of (useless) data duplication in the index, with a consequent decrease of performance when products are saved and later indexed in multiple indexes.
In particular, the duplication is more evident as the changes performed by the Processor are merely an alteration of:

  1. the query sent to Solr;
  2. the handling of the raw data returned by Solr.

This shows that there would be no need to index multiple times the same data.

Since the the possibility to define per-query processor sounded really promising and such feature could be used extensively in the same project, a new module has been implemented and published on Drupal.org: the SearchAPI Extended Processors module. (thanks to SearchAPI’s maintainer, DrunkenMonkey, for the help and review :) ).

The Drupal SearchAPI Extended Processor

The new module allows to extend the standard SearchAPI behavior for Processors and lets admins configure the execution of SearchAPI Processors per query and not only per-index.

By using the new module, any index can now be used with multiple and different Processors configurations, no new indexes are needed, thus avoiding data duplication.

The new configuration is exposed, as visible in the following picture, while editing a SearchAPI view under “Advanced > Query options”.
The SearchAPI processors can be altered and re-defined for the given view, a checkbox allows to completely override the current index setting rather than providing additional processors.

Drupal SearchAPI: view's extended processor settings

Image 2: View’s “Query options” with the SearchAPI Extended Processors module.

Conclusion: the new SearchAPI Extended Processors module has now been used for a few months in a complex eCommerce project at Liip and allowed us to easily implement new search features without the need to create multiple and separated indexes.
We are able to index Products data in one single (and compact) Solr index, and use it with different grouping strategies to build both product listings, model listings and model-category navigation pages without duplicating any data.
Since all those listings leverages the Solr FilterQuery query parameter to filter the correct set of products to be displayed, Solr can make use of its internal set of caches and specifically the filterCache to speed up subsequent searches and facets. This aspect, in addition to the usage of only one index, allows caches to be shared among multiple listings, and that would not be possible if separate indexes were used.

For further information, questions or curiosity drop me a line, I will be happy to help you configuring Drupal SearchAPI and Solr for your needs.

Apr 21 2016
Apr 21

The two biggest players in the Drupal 7 webshop field are Drupal Commerce (also known as DC1) and Übercart. DC1 actually started as an Übercart rewrite to make use of Drupal 7 APIs. After the split Übercart was ported to Drupal 7 too but it was still using Drupal 6 technologies.

Although still very much in development, it seems something similar will be true for Drupal 8 as well. The developers of DC2 (the Drupal 8 version of Drupal Commerce), lead by Bojan Živanović rewrote the whole system from scratch to make use of the huge changes in Drupal 8. They are active members of the Drupal developer community so they not only know but also form the actual best practices. While working on DC2 they have fixed many dozens of Drupal 8 core issues and much more in other contributed modules (such as Entity, Inline Entity Form, Profile).

A great realisation when rewriting Commerce was that several components of a webshop could be reused by other (not even necessarily webshop or Drupal) systems. Some typical examples are address formats, currencies and taxes. These components are usually a huge pain to maintain because of the small differences from country to country. So they have created standalone PHP libraries usually using authorative third party datasets such as CLDR for currency or Google’s dataset for address formats. Some of them are already used by other webshop solutions like Foxycart and developers even outside the Drupal community are giving feedback which makes maintaining them easier.

In the DC2 development process UI and UX has got a big emphasis already from the beginning. Based on research of existing webshop solutions the shop administration and checkout process has been redesigned by UX specialists. For example, the product creation process is quite confusing in DC1 and there’s not even a recommended way to do it. In DC2 this happens now in one single form which makes it super easy.

A new concept in DC2 is Stores. Stores represent billing locations and products can belong to one ore more stores. One use case is the need for different billing for customers from different countries. Another one is having a shop where sellers can open an account and sell their own products. In this case each seller has their own store.

There are many other new features and improvements like a new and flexible tax system (you can say things like: “from Jan 1st 2014 the VAT changes from 21% to 19%”), a redesigned checkout flow, different workflows for different order types etc.

DC2 is still in alpha phase and is not recommended for production use yet. Beta releases will already have upgrade paths between them and so can be considered for starting real sites with. Beta1 is expected for May.

Drupal Commerce is the most popular e-commerce solution for Drupal 7. Given the high quality code and responsiveness to developer, shop maintainer and customer needs I do not expect this to change in Drupal 8 either.

Sources:
Drupal Commerce 2 blog
Modules Unraveled podcast on Commerce

Feb 15 2015
Feb 15

Two new drupal distributions available on githubfrong

** https://github.com/alibama/cvillecouncilus is the distribution behind https://www.cvillecouncil.us - it’s an attempt to run a political campaign through a virtual proxy…

** https://github.com/alibama/rapid-prototyping-ecommerce-drupal – this is the code behind http://rpl.mae.virginia.edu/ it’s an e-commerce solution for 3d printing… A lot of this is implemented in rules and other well-standardized code thanks to Joe Pontani - a talented developer here in Virginia.  Joe integrated several third party tools, and set up the UVa payment gateway through Nelnet.

Both sites are getting updates over the next few months – the Charlottesville Council website also has a drupalgap implementation on it – absolutely awesome toolset…

18F API compliance is another feature I’m pretty stoked about… I got most of that done with the oauth2 server, views datasource, services and a couple of great notification features done with rules + views  i’ll get that feature out asap = it’s really convenient – matching a profile2 taxonomy field onto content taxonomy fields for notifications with new content.

any questions – please drop a line in the comments below

May 22 2013
May 22

We just launched Zact, one of our largest design projects to date at Chapter Three. We designed nearly 200 comps, including an e-commerce workflow, a customer dashboard that mirrors the functionality of the phone’s software, a Support section built on ZenDesk, and a consumer-facing website.

A disruptive new cell phone provider, Zact is a new company looking to redefine how customers purchase mobile services by making your plan 100% customizable right from your phone with no overage fees or contracts. They even give you a refund every month for any unused minutes, texts or data.

Helping Zact overcome business hurdles
As a new company in a major market, Zact turned to Chapter Three to help them solve some of their immediate business hurdles online.

  • Establishing brand trust
    To overcome lack of brand recognition and to educate new customers about the key advantages of the service, we created the “Why we're different” and “How it works” sections as a way for new customers to get to know us.
  • Paying full price for the phone
    To educate customers about the long term savings of buying the phone at full price, we created an interactive Savings Calculator. The calculator allows customers to compare various plan and phone options to their current bill to show their dollar amount saved over a two year period.
  • Buying a phone online
    Without the ability to physically touch the phone customers are buying, we needed to build in extra guarantees to make customers feel comfortable purchasing a device online. We featured a “satisfaction guarantee” statement prominently throughout the site, promising a refund within 30 days if the customer did not like the phone.

Herculean feats of UX strength
The complexity of interactions across the site gave us an opportunity to flex our UX chops. We collaborated with Zact’s usability specialist, incorporating feedback from weekly usability tests to iteratively improve our designs.

  • Customer dashboard
    To provide the functionality of the phone’s software on the website, we designed a web-specific interpretation of the phone software that empowers customers to access and control the full breadth of Zact’s service offerings. Because the software was being developed in parallel with our web design, we adopted an agile design approach to iterate in sync with the development team.
  • E-commerce
    Our team worked with Zact’s usability specialist to implement a checkout flow pulling from best practices across the web. We delivered a solution that pushes the capabilities of Drupal Commerce and its ability to integrate with third-party systems.

Agile design
An agile design process was critical in the success of this project. We needed to be flexible as requirements and scope were changing daily. We met with the client daily via WebEx with new design deliverables for review, which allowed us to gather feedback often and respond quickly. For any given page, we were able to explore a number of options on a high level before focusing on a more final solution.

In fact, some of the best ideas on the project came directly from the client, as a result of organic discussion during those meetings. The Savings Calculator, which allows users to more visually understand how they will save money over time with Zact, grew out of a conversation we facilitated.

Our first iterations of the Savings Calculator were pretty skeletal and didn’t quite feel right; the user had to fill out the form and click a button before seeing results. After further discussion, the client suggested that we make the actual dollar savings visible and dynamic throughout the page, so that as you interact with the form you can directly see how your savings are affected. This minor design change immediately made the page more engaging and an effective tool in communicating why Zact is a viable alternative to a traditional phone contract.

Starting up in Silicon Valley with Drupal
One of the most exciting and challenging parts of the project was the rapid pace of startup culture. The level of expertise and web savvy amongst Zact’s staff allowed for a flourishing partnership where we were able to push boundaries and do great work together. So far, the site has been covered by some major press outlets, including Gizmodo, Engadget, Forbes and TechCrunch.

The site is finally live, but our work isn’t over yet. We’re continuing to evaluate and optimize the usability of the site and will continue to roll out design updates over the coming weeks. We look forward to working further with Zact and seeing how users will react to the new site.

Mar 30 2013
Mar 30

Few month ago I developed  a simple module called Commerce Order2pdf and then I also added custom tokens as you can read from this post Custom token creation for Drupal 7 and now I reviewed this module a bit and planned to add views field handler (This means I can create custom view with download links). 

So how did I implemented this?

First step was to edit commerce_order2pdf.module file and add hook_views_api. So I could start working with views.

/**
 * Implements hook_views_api().
 */
function commerce_order2pdf_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'commerce_order2pdf') . '/includes/views',
  );
}

Next one was to create subdirectories includes/views into my commerce_order2pdf folder (module folder).

After this I had to tell views about my data structure.
So I did created new file /includes/views/commerce_order2pdf.views.inc and added hook_views_data.

/**
 * Implements hook_views_data()
 */
function commerce_order2pdf_views_data() {
  $data = array();
  $data['commerce_order']['order2pdf_link'] = array(
    'field' => array(
      'title' => t('Download pdf order'),
      'help' => t('Provide a simple link to download the order.'),
      'handler' => 'commerce_order2pdf_handler_field_order_pdf_link',
    ),
  );
  return $data;
 }

In this array I did extended commerce_order group and added field handler class callback.

Now we will add a new file commerce_order2pdf_handler_field_order_pdf_link.inc to includes/views folder.

/**
 * Field handler to present an order pdf download link.
 */
class commerce_order2pdf_handler_field_order_pdf_link extends commerce_order_handler_field_order_link {
  function construct() {
    parent::construct();
  }

  function render($values) {
    $order = commerce_order_new();
    $order->order_id = $this->get_value($values, 'order_id');
    // Add hash key for a download link.
    $hash = hash('md5', $order->order_id . $values->commerce_order_created);

    $text = !empty($this->options['text']) ? $this->options['text'] : t('Download');
    return l($text, "order2pdf/{$order->order_id}/{$hash}");
  }
}

My last step is to say Drupal about these files in commerce_order2pdf.info

For this step I will just add two rows to my .info file. 

files[] = includes/views/commerce_order2pdf.views.inc
files[] = includes/views/commerce_order2pdf_link_handler.inc

Now you can add views download field to your view. (If you make thses changes after you enabled your module then you can just clear caches and it will work).

Edit 13. April 2013 
Just found a good videos

[embedded content]

[embedded content]

Apr 01 2012
Apr 01

Under.me is probably THE sexiest Drupal website around, both for it's amazing front-end technology and UI, as well as for it's so-good-looking models: David Balsar and Bar Refaeli, the world famous top-model.
Under.me is the online store for their new brand of underwear, designed and demonstrated by Bar Refaeli.

It is a state-of-the-art website, with the latest best practices in Drupal and Front-End development. Using Sasson, an HTML5-CSS3-SASS Drupal theme, and a lot of jquery magics, dictated by the fine UI/UX work by Eyal Shahar, we are able to deliver a new and unique experience of online shopping. Within a couple of clicks, the visitor is able to browse the catalog and add product to the cart.

Under.me is a light-weight one-page catalog, which aims to create the most pleasant shopping experience, with as little fuss as possible - easily add and remove items to the cart, and never lose focus. Bar's presence in every step of the purchase process is communicating credibility blended with fun and fresh spirit.

To make it even easier for returning customers, Under.me offers a unique concept of subscriptions to packages, so one can periodically receive new items, without te need to go back to the online-shop and re-execute the same purchase - this is perfect for anyone who likes their shopping experience to be as comfortable and fit as their underwear clothing is :)

Sep 12 2011
Sep 12

Ubercart-Customizing shopping cart for Marketplace Website

Posted on: Monday, September 12th 2011 by Rexx Llabore

I am currently working on a website which requires me to group items on the cart by seller. The shopping cart should also display the subtotal and a submit button for each seller.

Ubercart is designed assuming that your website is the only store. However, it is very flexible that you can support multiple stores/merchants on your site. There are many ways in which you can customize Ubercart. When you do, it is important to follow Ubercart methodologies first, by using Ubercart defined hooks. If no Ubercart hook is available, then use Drupal methodologies.

Here are the steps that I took in order to customize the shopping cart page:

1.Implement hook_add_to_cart_data() - When an item is added to the cart, I need to be able to retrieve the seller user id or the store node id that the item is associated with. Since the owner of the item will unlikely change, I might as well put the seller user id or store node in the data array of an item.

2.Implement hook_TYPE_alter(), (TYPE = 'uc_cart') – Ubercart allows developers to alter items loaded from the database (see uc_cart_get_contents()). To make it easier for me to display items grouped by seller, I had to alter how raw data was arranged. On the cart page, I rearranged the items data structure so that they are grouped by seller already before it is processed at the form level.

3.Implement hook_TYPE_alter(), (TYPE = 'cart_pane') – The cart page is designed so that other modules can display their information on the page using panes. I needed to override the “cart_form” pane so that it would display my custom cart form which was derived from what Ubercart is using by default.

4.Implement my custom cart form – The reason why implemented my own custom cart form and not use hook_form_alter is because in order to display the desired output, I had to restructure the form data. The default cart form uses Table API to display the items on the cart. By default, there is only one table API element which holds all items on the cart. Since I had to group items by seller, the same number of table API elements should equal the number sellers.

5.Implement my custom table definition function (see uc_cart_view_table) – I had to use my own cart view table function which is an argument in “tapir_get_table”. “tapir_get_table” function is responsible for building a table API element. I implemented my own table definition function because I had to add the “Price” column.

6.Implement the theme function of my custom cart form – The my theme function is derived from the theme function of default cart form. Since the form structure has been altered dramatically, I had to modify my theme function accordingly.

SAMPLE CODE:

STEP 1:


/**
 * Implementation of hook_add_to_cart_data()
 */
function MY_MODULE_add_to_cart_data($form_values) {
  $node = node_load($form_values['nid']);
  return array('uid' => $node->uid);
}

STEP 2:


/**
 * Implementation of hook_TYPE_alter().
 * NOTE: TYPE == 'uc_cart'
 */
function MY_MODULE_uc_cart_alter(&$items) {
   $cart_page  = 'cart';
	
   /* before showing to user, rearrange the items by owner; on cart page, I will
       control how the items data structure will be formatted */
   if($_GET['q'] == $cart_page) {
      $temp_basket = array();
      foreach($items as $i => $item) {
         $temp_basket[$item->data['uid']][] = $item;
      }
      $items  = array();		
      $weight = 0;	
      foreach($temp_basket as $uid => $_items) {
         foreach($_items as $_item) {
	    $_item->weight = $weight++; 
	 }
      }	
      $items = $temp_basket;	
   }
}

STEP 3:


/**
 * Implementation of hook_TYPE_alter().
 * NOTE: TYPE == 'cart_pane'
 */
function MY_MODULE_cart_pane_alter(&$panes, $items) {
   if(!count($panes) || !count($items)) {
      return;
   }
	
   foreach($panes as $k => &$pane) {
      if($pane['id'] == 'cart_form') {
         $cart_form = !is_null($items) ? '<div id="cart-form-pane">'. drupal_get_form('MY_MODULE_view_cart_form', $items) .'</div>': '';
			
         $pane['body'] = $cart_form;
      }
   }
}

STEP 4:


/**
 * Custom implementation of cart form.
 * @see uc_cart_view_form()
 */
function MY_MODULE_view_cart_form($form_state, $items = NULL) {
	
   $form = array();
   $i    = 0;
	
   /* items in the cart are grouped by node owner */
   foreach ($items as $owner_uid => $owner_items) {
		
      /* seller link */
      $account = user_load($owner_uid);
      $link    = l($account->name, 'profile/'. $account->uid);
		
      $form[$owner_uid]['seller'] = array(
         '#value' => t('Order from ') . $link,
      );
		
      /* build tapir table element; contains items from each seller */
      $form[$owner_uid]['items'] = array(
         '#type' => 'tapir_table',
	 '#tree' => TRUE,
      );
	
      $context = array(
         'revision' => 'themed-original',
	 'type' => 'amount',
      );
		
      /* loop through each item that belongs to the current owner */
      foreach($owner_items as $item) {
			
         display_item = module_invoke($item->module, 'cart_display', $item);
			
	 if (!empty($display_item)) {
			
	    /* its OK to node_load each product since by the time this gets called,
	       the node has been statically cached */
	    $product_node = node_load($display_item['nid']['#value']);
				
	    /* modify the image size */
	    $image = theme('imagecache', 'MY_MODULE_product_preset', $product_node->field_image_cache[0]['filepath']);
	    $l_path    = 'node/'. $product_node->nid;
	    $l_options = array(
	       'html' => TRUE,
	    );

	    $form[$owner_uid]['items'][$i] = $display_item;
	    $form[$owner_uid]['items'][$i]['image']['#value'] =  l($image, $l_path, $l_options);
	
	    $description = $display_item['title']['#value'] . $display_item['description']['#value'];
	    $form[$owner_uid]['items'][$i]['desc']['#value'] = $description;
	
	    $form[$owner_uid]['items'][$i]['title']['#type'] = 'value';
	    $form[$owner_uid]['items'][$i]['description']['#type'] = 'value';
	
	    if (empty($display_item['qty'])) {
	       $form[$owner_uid]['items'][$i]['qty'] = array(
	          '#value' => '',
	       );
	    }
				
	    $form[$owner_uid]['items'][$i]['price']['#value'] = sprintf('$%01.2f',$item->price);
				
	    $form[$owner_uid]['items'][$i]['total'] = array(
	       '#value' => uc_price($display_item['#total'], $context),
	       '#theme' => 'uc_cart_view_price',
	    );
	    $i++;
         }
      }
		
      /* build the tapir element for the given seller */
      $form[$owner_uid]['items'] = tapir_get_table('MY_MODULE__helpers_view_cart_table', $form[$owner_uid]['items']);
	
      /* submit order button for each seller */
      $form[$owner_uid]['submit_order'] = array(
         '#type'   => 'submit',
	 '#value'  => t('Submit Order'),
	 '#submit' => array('MY_MODULE__forms_cart_order_submit'),
	 '#name'   => $owner_uid
      );
   }

  // If the continue shopping element is enabled...
  if (($cs_type = variable_get('uc_continue_shopping_type', 'link')) !== 'none') {
    // Setup the text used for the element.
    $cs_text = variable_get('uc_continue_shopping_text', '') ? variable_get('uc_continue_shopping_text', '') : t('Continue shopping');

    // Add the element to the form based on the element type.
    if (variable_get('uc_continue_shopping_type', 'link') == 'link') {
      $form['continue_shopping'] = array(
        '#value' => l($cs_text, uc_cart_continue_shopping_url()),
      );
    }
    elseif (variable_get('uc_continue_shopping_type', 'link') == 'button') {
      $form['continue_shopping'] = array(
        '#type' => 'submit',
        '#value' => $cs_text,
        '#submit' => array('uc_cart_view_form_submit'),
      );
      $form['continue_shopping_text'] = array(
        '#type' => 'hidden',
        '#value' => $cs_text,
      );
    }
  }

  // Add the control buttons for updating and proceeding to checkout.
  $form['update'] = array(
    '#type' => 'submit',
    '#value' => t('Update cart'),
    '#submit' => array('uc_cart_view_form_submit'),
  );
  return $form;
}

STEP 5:


/**
 * List the products in the cart in a TAPIr table.
 * @see uc_cart_view_table()
 */
function MY_MODULE__helpers_view_cart_table($table) {
  $table['#attributes'] = array('width' => '100%');

  $table['#columns'] = array(
    'remove' => array(
      'cell' => t('Remove'),
      'weight' => 0,
    ),
    'image' => array(
      'cell' => t('Products'),
      'weight' => 1,
    ),
    'desc' => array(
      'cell' => '',
      'weight' => 2,
    ),
    'qty' => array(
      'cell' => t('Qty.'),
      'weight' => 3,
    ),
		'price' => array(
      'cell' => t('Price'),
      'weight' => 3,
    ),
    'total' => array(
      'cell' => t('Total'),
      'weight' => 4,
    ),
  );

  foreach (element_children($table) as $i) {
    $subtotal += $table[$i]['#total'];

    $table[$i]['remove']['#cell_attributes'] = array('align' => 'center', 'class' => 'remove');
    $table[$i]['image']['#cell_attributes'] = array('class' => 'image');
    $table[$i]['desc']['#cell_attributes'] = array('class' => 'desc');
    $table[$i]['qty']['#cell_attributes'] = array('class' => 'qty');
    $table[$i]['price']['#cell_attributes'] = array('class' => 'qty');
    $table[$i]['total']['#cell_attributes'] = array('class' => 'price');
    $table[$i]['#attributes'] = array('valign' => 'top');
  }

  $context = array(
    'revision' => 'themed-original',
    'type' => 'amount',
  );
	
  /* format subtotal per order */
  $subtotal_text = t('Subtotal');
  $shipping_text = t('shipping not included');
  $price         = uc_price($subtotal, $context);
  $approx_text   = t('approximately');
  $total_text ="
    <strong>$subtotal_text</strong> ($shipping_text):<strong><span class="cart-price">$price USD</span></strong><br/>
    $approx_text<br/>
    $price usd
";
	
  $table[] = array(
    'total' => array(
      '#value' => $total_text,
      '#cell_attributes' => array(
        'colspan' => 'full',
        'align' => 'right',
        'class' => 'subtotal',
      ),
    ),
  );
  return $table;
}

STEP 6:


/**
 * @ingroup themeable
 * @see uc_cart_view_form()
 */
function theme_MY_MODULE_view_cart_form($form) {
  drupal_add_css(drupal_get_path('module', 'uc_cart') .'/uc_cart.css');
	
  $output = '';
  foreach($form as $id => $info) {
		
    if(is_numeric($id)) {
			
      $seller  = drupal_render($form[$id]['seller']);
      $items   = drupal_render($form[$id]['items']);
      $submit  = drupal_render($form[$id]['submit_order']);
			
      $seller ="
	<div class="seller-cart-title">
	  $seller
	</div>
";
      $items ="
	<div class="cart-form-products">
	   $items
	</div>
";
      $submit ="
	<div class="cart-submit round">
	  $submit
	</div>
";
      $order ="
	<div class="cart-order clearfix">
	  $seller
	  $items
	  $submit
	</div>
";
      $output.= $order;
			
      /* make sure that these elements don't get displayed again */
      foreach(element_children($form[$id]['items']) as $i) {
	foreach (array('title', 'options', 'remove', 'image', 'qty', 'price') as $column) {
	  $form[$id]['items'][$i][$column]['#printed'] = TRUE;
	}
	$form[$id]['items'][$i]['#printed'] = TRUE;
      }
    }
  }
	
 
  /* Add the continue shopping element and cart submit buttons. */
  if (($type = variable_get('uc_continue_shopping_type', 'link')) != 'none') {
		
    /* Render the continue shopping element into a variable. */
    $cs_element = drupal_render($form['continue_shopping']);

    /* Add the element with the appropriate markup based on the display type. */
    if ($type == 'link') {
      $rendered_form = drupal_render($form);
			
      $output .="
	<div id="cart-form-buttons">
	  <div id="continue-shopping-link">
	    $cs_element
	  </div>
	  $rendered_form
	</div>
";
    }
    elseif ($type == 'button') {
      $rendered_form = drupal_render($form);
			
      $output .="
	<div id="cart-form-buttons">
	  <div id="update-checkout-buttons">
	  $rendered_form
	  </div>
	  <div id="continue-shopping-button">
	   $cs_element
	  </div>
	</div>
";
    }
  }
  else {
     $rendered_form = drupal_render($form);
		
     $output .="
       <div id="cart-form-buttons">
         $rendered_form
       </div>
";
  }

  return $output;
}

Apr 10 2010
Lev
Apr 10

PastedGraphic1_eIio1n3GgsC8_Iw0ZkR7vmQEJ.jpgSorry, I couldn’t resist the play on the words. This storied Minneapolis nightclub has been a music mecca since the 70’s, entering the national spotlight in the mid 80’s when it helped launch Prince and was the venue for the classic movie Purple Rain. Level OS first launched the site around 6 years ago on a custom ASP.NET platform (cut me some slack, it was my first project with my own company!). It was migrated to Drupal about 2 and a half years ago, right around v5.2. That version of the site featured a custom Ubercart integration allowing the club to sell both tickets and sightline seats to events, along with regular merchandise, heavy use of CCK and Views for features like blogs, forums, and galleries, sample audio tracks, and an online community. In the ensuing two years, both the code base and design lacked the flexibility the site needed, so we updated both. Side note: It’s amazing what passed in my book for Drupal best practices 3 years ago! The upgrade process was, well, extremely painful to be blunt, and this coming from a fairly experienced Druapler. The challenges we encountered included:

  • Some problems migrating modules from D5 to D6 versions. E.g., Ubercart changed the structure of a products data object, but existing data wasn’t updated and the change wasn’t clearly documented. The changes were not complex, but they were mostly only discovered through trial and error.
  • Migrating from the Event module to Date/Calendar/CCK.
  • Theme updates
  • Custom module updates
  • Migrating to the use of true best practices
  • The site is fairly heavily trafficked, with lots of new content daily, so nearly all the configuration updates were codified.

The new site, while still having lots of room for improvement, is finally coming into its own and features the following.

  • Still using Ubercart, now in a much more mature state, for selling tickets and reserved table seats for select events, both of which have their own inventories, along with merchandise. The former is accomplished by taking advantage of a few UC hooks along with product attributes.
  • Event system with JQuery carousels on the front page, listings grouped by day with heavily themed rows, and iCal feeds. The usual tools were employed, including CCK, Date, Calendar, and Views modules.
  • Very active Blog
  • Venues, with mapped locations and a list of events in each venue

Big thanks to Annette Brooks on her amazing design skills and to RJ Steinert for his theming help. Happy to answer any questions about implementation or the tools that were used, and, of course, feedback very welcome.

Jan 20 2010
Lev
Jan 20

The Journey Gym is a exciting and innovative entry in the portable workout field just launched by Portland based startup Journey Fitness, LLC. It's been great working with their team launching the new site promoting and selling the gym. The site features:

  • Shopping cart powered by Ubercart.
  • Nutrition blog
  • Custom theme development, done in part by Eternalistic Designs.
  • Javascript rotating image galleries
  • Intro video displayed via a lightbox, streamed from the site using Filefield.
  • An FAQ section powered by the module of the same name.

Stay tuned for an online community and a system to put together your own workouts from a collection of exercise clips.

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web