Dimensional Support Installation
--------------------------------

This UPSXML module features an optional extension to the osCommerce shipping system
designed to increase the accuracy of shipping quotations. It does this by adding
length, width, and height to the products definition, an administrative interface
by which one defines shipping containers and their dimensions, plus a rudimentary
packing algoritm to efficiently pack products into those containers. With this
extension, UPS obtains more accurate information on product weight and shipping
dimensions and returns more accurate quotes.

Two benefits arise from more accurate shipping quotes:

1) It reduces overquoting which deters customers. More resonable rates make the price more attractive.
2) It reduces underquoting, especially now that UPS applies dimensional weight to all services, which saves the business money. Underquoted rates are great for the customer, but costs the business money when the UPS bill comes in at the end of the month.

The package definition is located in Admin->Tools->Packaging, and should be pretty straightforward. 
Cost can be set in the packaging but is not used anywhere, so don't bother. The algorithm needs boxes sorted to volume. Cost just disturbs this. Anyway, you are smart enough to use the cheapest boxes yourself.

The units you define your product with are the same units you should configure in the
admin (Configuration->Shipping/packaging) (i.e. If you configure to LBS/IN, your products should be similarly measured.)

Remember that the algorithms used are rudimentary. If for example the customer has as the first product in her cart one that fills 60% of the largest box, then one that fills 50% of the largest box, and then smaller packages, the smaller packages will go into the second box but will overflow into a new one if the second one is filled. The algorithm will *not* go back to filled boxes to see if any room is left.
If products are relatively large compared to the size of the box the algorithms might pack more product in the box than physically possible. For example if you pack products of 5 x 5 x 10 inch in a box of 10 x 10 x 18 inch the module will fit 7 products (1800 / 250 = 7.2 and they fit in the box dimension wise) whereas physically you could only fit 4.

You can designate a product as "Ready-to-ship". This will mean it ships in its own box of the size you specify for the product. The algorithm will not try to put it in another box.
This is, for example, interesting if you have products that are oversized according to the UPS rules.

Starting from UPSXML version 1.2 you can choose three options: to not use this Dimensional Support, only use Ready-to-ship and full dimensional support. In the second option the ready-to-ship products will be quoted for as separate packages with a dimension. Remaining products will be calculated in the standard osC way.
With the third option: full dimensional support a few extra checks have been added. When a product according to weight or diagonal size does not fit the largest box, it will be designated ready-to-ship (otherwise the program will get into a loop). When one or several sizes of a product are zero, but not the weight it will be designated a size according to the weight, using $density = 0.7 (pretty arbitrary, defined as 0.7 kilograms/liter).

You might want to just calculate the density for an average shipment yourself and only measure and set the sizes for articles that are not average.

When you put in the sizes of your products, don't forget to add to the size the amount the styrofoam peanuts or other packing material will take up around it.

If you want to split a product in several items for packaging (only useful in combination with ready-to-ship or full dimensional support) you will need to first make the changes described below and then continue with the changes in split_products_instructions.txt.

If you want to see/check the results of the packaging algorithm follow the directions in the file store_ups_boxes_used_instructions.txt (after you have followed the directions below).

To enable dimensional support, several sub-steps must be followed. You backed up
your database, right?

--------
Step 1
--------

Modify your database tables using the install_upsxml_dimensions.sql instructions.

This modification will create several new columns in the products table, as well as
create a whole new table to house the package definitions.

To remove the sql you can use the file uninstall_upsxml.sql

--------
Step 2
--------

Omitted, is an admin setting now under the heading "Dimensions Support" (Admin->Configuration->Shipping/Packaging) which you added when you installed UPSXML.
Don't change the setting from "No" to something else unless you have done the below up to and especially including step 9.

--------
Step 3
--------

*** Modify catalog/includes/classes/shopping_cart.php as follows ***

A) At about line 319 [in the function get_products()], change the $products_query to read:

$products_query = tep_db_query("select p.products_id, pd.products_name, p.products_model, p.products_image, p.products_price, p.products_weight, p.products_length, p.products_width, p.products_height, p.products_ready_to_ship, p.products_tax_class_id from " . TABLE_PRODUCTS . " p, " . TABLE_PRODUCTS_DESCRIPTION . " pd where p.products_id = '" . (int)$products_id . "' and pd.products_id = p.products_id and pd.language_id = '" . (int)$languages_id . "'");

  The difference being the addition of four additional fields:

	  ", p.products_length, p.products_width, p.products_height, p.products_ready_to_ship"

B) At about line 336 [still in the function get_products()]
 
  insert after the line

          'weight' => $products['products_weight'],

  the following additional four lines:

          'length' => $products['products_length'],
          'width' => $products['products_width'],
          'height' => $products['products_height'],
          'ready_to_ship' => $products['products_ready_to_ship'],


*** Modify catalog/admin/includes/classes/shopping_cart.php as follows ***

A) At about line 260, change the $products_query to read [in the function get_products()]:

$products_query = tep_db_query("select p.products_id, pd.products_name, p.products_model, p.products_image, p.products_price, p.products_weight, p.products_length, p.products_width, p.products_height, p.products_ready_to_ship, p.products_tax_class_id from " . TABLE_PRODUCTS . " p, " . TABLE_PRODUCTS_DESCRIPTION . " pd where p.products_id = '" . (int)$products_id . "' and pd.products_id = p.products_id and pd.language_id = '" . (int)$languages_id . "'");

  The difference being the addition of four additional fields:

	  ", p.products_length, p.products_width, p.products_height, p.products_ready_to_ship"

B) At about line 276 [in the function get_products()]
 
  insert after the line

          'weight' => $products['products_weight'],

  the following additional four lines:

          'length' => $products['products_length'],
          'width' => $products['products_width'],
          'height' => $products['products_height'],
          'ready_to_ship' => $products['products_ready_to_ship'],

--------
Step 4
--------

*** Modify catalog/admin/categories.php as follows ***
Alternatively use the RC2a compatible admin/categories.php included, this file also includes the code for split_products but you can easily delete these (lines 614-626) if you don't need that.

A) At or near line 222 [in case 'update_product':], after the line:

	 'products_weight' => (float)tep_db_prepare_input($HTTP_POST_VARS['products_weight']),

    insert the lines:

	 'products_length' => (float)tep_db_prepare_input($HTTP_POST_VARS['products_length']),
	 'products_width' => (float)tep_db_prepare_input($HTTP_POST_VARS['products_width']),
 	 'products_height' => (float)tep_db_prepare_input($HTTP_POST_VARS['products_height']),
	 'products_ready_to_ship' => (isset($HTTP_POST_VARS['products_ready_to_ship']) && (tep_db_prepare_input($HTTP_POST_VARS['products_ready_to_ship']) == '1') ? 1 : 0),

B) At or near line 295-299 [in case case 'copy_to_confirm':], replace the lines (the first line is not changed but shown here for orientation):

          } elseif ($HTTP_POST_VARS['copy_as'] == 'duplicate') {
            $product_query = tep_db_query("select products_quantity, products_model, products_image, products_price, products_date_available, products_weight, products_tax_class_id, manufacturers_id from " . TABLE_PRODUCTS . " where products_id = '" . (int)$products_id . "'");
            $product = tep_db_fetch_array($product_query);

            tep_db_query("insert into " . TABLE_PRODUCTS . " (products_quantity, products_model,products_image, products_price, products_date_added, products_date_available, products_weight, products_status, products_tax_class_id, manufacturers_id) values ('" . tep_db_input($product['products_quantity']) . "', '" . tep_db_input($product['products_model']) . "', '" . tep_db_input($product['products_image']) . "', '" . tep_db_input($product['products_price']) . "',  now(), " . (empty($product['products_date_available']) ? "null" : "'" . tep_db_input($product['products_date_available']) . "'") . ", '" . tep_db_input($product['products_weight']) . "', '0', '" . (int)$product['products_tax_class_id'] . "', '" . (int)$product['manufacturers_id'] . "')");
            $dup_products_id = tep_db_insert_id();

**WITH**

          } elseif ($HTTP_POST_VARS['copy_as'] == 'duplicate') {
            // product table copy modified to copy ALL added fields
            $product_query = tep_db_query("select * from " . TABLE_PRODUCTS . " where products_id = " . (int)$products_id);
            $product = tep_db_fetch_array($product_query);
            unset($product['products_id']); // remove the original product id, this is automatically replaced with a new id
            $product['products_status'] = 0; // duplicate starts as not displayed in catalog as with original osCommerce code
            $product['products_date_added'] = 'now()'; // duplicate product is added now
						$product['products_last_modified'] = 'null'; // duplicate product has not been modified yet
            if (empty($product['products_date_available'])) $product['products_date_available'] = 'null'; // if empty keeps duplicate blank instead of 0000-00-00
            tep_db_perform(TABLE_PRODUCTS, $product); // insert the duplicate product
            $dup_products_id = tep_db_insert_id();

C) At or near line 373, after the line:

                       'products_weight' => '',

     insert the lines:

                       'products_length' => '',
                       'products_width' => '',
                       'products_height' => '',
                       'products_ready_to_ship' => '',

D) At or near line 388, change the line:

      $product_query = tep_db_query("select pd.products_name, pd.products_description, pd.products_url, p.products_id, p.products_quantity, p.products_model, p.products_image, p.products_price, p.products_weight, p.products_date_added, p.products_last_modified, date_format(p.products_date_available, '%Y-%m-%d') as products_date_available, p.products_status, p.products_tax_class_id, p.manufacturers_id from " . TABLE_PRODUCTS . " p, " . TABLE_PRODUCTS_DESCRIPTION . " pd where p.products_id = '" . (int)$HTTP_GET_VARS['pID'] . "' and p.products_id = pd.products_id and pd.language_id = '" . (int)$languages_id . "'");

    to

      $product_query = tep_db_query("select pd.products_name, pd.products_description, pd.products_url, p.products_id, p.products_quantity, p.products_model, p.products_image, p.products_price, p.products_weight, products_length, products_width, products_height, products_ready_to_ship, p.products_date_added, p.products_last_modified, date_format(p.products_date_available, '%Y-%m-%d') as products_date_available, p.products_status, p.products_tax_class_id, p.manufacturers_id from " . TABLE_PRODUCTS . " p, " . TABLE_PRODUCTS_DESCRIPTION . " pd where p.products_id = '" . (int)$HTTP_GET_VARS['pID'] . "' and p.products_id = pd.products_id and pd.language_id = '" . (int)$languages_id . "'");

E) At or near 593-596, change
          <tr>
            <td class="main"><?php echo TEXT_PRODUCTS_WEIGHT; ?></td>
            <td class="main"><?php echo tep_draw_separator('pixel_trans.gif', '24', '15') . '&nbsp;' . tep_draw_input_field('products_weight', $pInfo->products_weight); ?></td>
          </tr>

    to
        
          <tr>
            <td class="main"><?php echo TEXT_PRODUCTS_WEIGHT; ?></td>
            <td class="main"><?php echo tep_draw_separator('pixel_trans.gif', '24', '15') . '&nbsp;' . tep_draw_input_field('products_weight', $pInfo->products_weight, 'min="0" step="0.01"', false, 'number'); ?></td>
          </tr>
          <tr>
            <td class="main"><?php echo TEXT_PRODUCTS_LENGTH; ?></td>
            <td class="main"><?php echo tep_draw_separator('pixel_trans.gif', '24', '15') . '&nbsp;' . tep_draw_input_field('products_length', $pInfo->products_length, 'min="0" step="0.001"', false, 'number'); ?></td>
          </tr>
          <tr>
            <td class="main"><?php echo TEXT_PRODUCTS_WIDTH; ?></td>
            <td class="main"><?php echo tep_draw_separator('pixel_trans.gif', '24', '15') . '&nbsp;' . tep_draw_input_field('products_width', $pInfo->products_width, 'min="0" step="0.001"', false, 'number'); ?></td>
          </tr>
          <tr>
            <td class="main"><?php echo TEXT_PRODUCTS_HEIGHT; ?></td>
            <td class="main"><?php echo tep_draw_separator('pixel_trans.gif', '24', '15') . '&nbsp;' . tep_draw_input_field('products_height', $pInfo->products_height, 'min="0" step="0.001"', false, 'number'); ?></td>
          </tr>
          <tr>
            <td class="main"><?php echo TEXT_PRODUCTS_READY_TO_SHIP; ?></td>
            <td class="main"><?php echo tep_draw_separator('pixel_trans.gif', '24', '15') . '&nbsp;' . tep_draw_checkbox_field('products_ready_to_ship', '1', ($pInfo->products_ready_to_ship == '1')) . '<small>' . TEXT_READY_SHIP_EXPLAIN . '</small>'; ?></td>
          </tr>


F) At or near line 643, change the line:

    	$product_query = tep_db_query("select p.products_id, pd.language_id, pd.products_name, pd.products_description, pd.products_url, p.products_quantity, p.products_model, p.products_image, p.products_price, p.products_weight, p.products_date_added, p.products_last_modified, p.products_date_available, p.products_status, p.manufacturers_id  from " . TABLE_PRODUCTS . " p, " . TABLE_PRODUCTS_DESCRIPTION . " pd where p.products_id = pd.products_id and p.products_id = '" . (int)$HTTP_GET_VARS['pID'] . "'");

    to

    	$product_query = tep_db_query("select p.products_id, pd.language_id, pd.products_name, pd.products_description, pd.products_url, p.products_quantity, p.products_model, p.products_image, p.products_price, p.products_weight, p.products_length, p.products_width, p.products_height, p.products_ready_to_ship, p.products_date_added, p.products_last_modified, p.products_date_available, p.products_status, p.manufacturers_id  from " . TABLE_PRODUCTS . " p, " . TABLE_PRODUCTS_DESCRIPTION . " pd where p.products_id = pd.products_id and p.products_id = '" . (int)$HTTP_GET_VARS['pID'] . "'");

G) Around line 863 FIND:

    if (isset($HTTP_GET_VARS['search'])) {
      $products_query = tep_db_query("select p.products_id, pd.products_name, p.products_quantity, p.products_image, p.products_price, p.products_date_added, p.products_last_modified, p.products_date_available, p.products_status, p2c.categories_id from " . TABLE_PRODUCTS . " p, " . TABLE_PRODUCTS_DESCRIPTION . " pd, " . TABLE_PRODUCTS_TO_CATEGORIES . " p2c where p.products_id = pd.products_id and pd.language_id = '" . (int)$languages_id . "' and p.products_id = p2c.products_id and pd.products_name like '%" . tep_db_input($search) . "%' order by pd.products_name");
    } else {
      $products_query = tep_db_query("select p.products_id, pd.products_name, p.products_quantity, p.products_image, p.products_price, p.products_date_added, p.products_last_modified, p.products_date_available, p.products_status from " . TABLE_PRODUCTS . " p, " . TABLE_PRODUCTS_DESCRIPTION . " pd, " . TABLE_PRODUCTS_TO_CATEGORIES . " p2c where p.products_id = pd.products_id and pd.language_id = '" . (int)$languages_id . "' and p.products_id = p2c.products_id and p2c.categories_id = '" . (int)$current_category_id . "' order by pd.products_name");
    }

CHANGE TO:

    if (isset($HTTP_GET_VARS['search'])) {
      $products_query = tep_db_query("select p.products_id, pd.products_name, p.products_quantity, p.products_image, p.products_price, p.products_weight, p.products_length, p.products_width, p.products_height, p.products_ready_to_ship, p.products_date_added, p.products_last_modified, p.products_date_available, p.products_status, p2c.categories_id from " . TABLE_PRODUCTS . " p, " . TABLE_PRODUCTS_DESCRIPTION . " pd, " . TABLE_PRODUCTS_TO_CATEGORIES . " p2c where p.products_id = pd.products_id and pd.language_id = '" . (int)$languages_id . "' and p.products_id = p2c.products_id and pd.products_name like '%" . tep_db_input($search) . "%' order by pd.products_name");
    } else {
      $products_query = tep_db_query("select p.products_id, pd.products_name, p.products_quantity, p.products_image, p.products_price, p.products_weight, p.products_length, p.products_width, p.products_height, p.products_ready_to_ship, p.products_date_added, p.products_last_modified, p.products_date_available, p.products_status from " . TABLE_PRODUCTS . " p, " . TABLE_PRODUCTS_DESCRIPTION . " pd, " . TABLE_PRODUCTS_TO_CATEGORIES . " p2c where p.products_id = pd.products_id and pd.language_id = '" . (int)$languages_id . "' and p.products_id = p2c.products_id and p2c.categories_id = '" . (int)$current_category_id . "' order by pd.products_name");
    }

H) Around line 1048 FIND:

            $contents[] = array('text' => '<br />' . TEXT_PRODUCTS_PRICE_INFO . ' ' . $currencies->format($pInfo->products_price) . '<br />' . TEXT_PRODUCTS_QUANTITY_INFO . ' ' . $pInfo->products_quantity);
            $contents[] = array('text' => '<br />' . TEXT_PRODUCTS_AVERAGE_RATING . ' ' . number_format($pInfo->average_rating, 2) . '%');

ADD between or below, your choice:

            $contents[] = array('text' => '<br />' . TEXT_PRODUCTS_WEIGHT . ' ' . $pInfo->products_weight . '<br />' . TEXT_PRODUCTS_LENGTH . ' ' . $pInfo->products_length . '<br />' . TEXT_PRODUCTS_WIDTH . ' ' . $pInfo->products_width . '<br />' . TEXT_PRODUCTS_HEIGHT . ' ' . $pInfo->products_height . '<br />' . TEXT_PRODUCTS_READY_TO_SHIP . ' ' . ($pInfo->products_ready_to_ship ? TEXT_YES : TEXT_NO));

      
Whew!!

--------
Step 5
--------

A) Modify the file catalog/admin/includes/languages/english.php, adding the line (for example around line 110):

	define('BOX_TOOLS_PACKAGING', 'Packaging');

B) Modify the file catalog/admin/includes/languages/english/categories.php

Add the following definitions (the file is arranged in alphabetical order)

  define('TEXT_PRODUCTS_LENGTH', 'Package Length:');
  define('TEXT_PRODUCTS_WIDTH', 'Package Width:');
  define('TEXT_PRODUCTS_HEIGHT', 'Package Height:');
  define('TEXT_PRODUCTS_READY_TO_SHIP', 'Ready to ship:');
  define('TEXT_READY_SHIP_EXPLAIN', ' Check if you can put a label on this product and ship it without additional packaging.');
define('TEXT_YES', 'Yes');
define('TEXT_NO', 'No');

--------
Step 6
--------

A) Modify catalog/admin/includes/filenames.php and add the following line:

  define('FILENAME_PACKAGING', 'packaging.php');

B) Modify catalog/admin/includes/database_tables.php and add the following line:

  define('TABLE_PACKAGING', 'packaging');

C) Modify catalog/includes/database_tables.php and add the following line:

  define('TABLE_PACKAGING', 'packaging');

--------
Step 7
--------

Modify catalog/admin/includes/boxes/tools.php:

Find the lines:

      array(
        'code' => FILENAME_SERVER_INFO,
        'title' => BOX_TOOLS_SERVER_INFO,
        'link' => tep_href_link(FILENAME_SERVER_INFO)
      ),

insert after, the lines:

      array(
        'code' => FILENAME_PACKAGING,
        'title' => BOX_TOOLS_PACKAGING,
        'link' => tep_href_link(FILENAME_PACKAGING)
      ),

--------
Step 8
--------

Install these other files from the distribution to their respective locations:

  catalog/includes/classes/packing.php
  catalog/admin/packaging.php
  catalog/admin/includes/languages/english/packaging.php

--------
Step 9
--------

The packaging routines are done outside of the shipping module in the class packing. It is instantiated when the page checkout_shipping is called (I tried to add it to includes/classes/shipping.php which makes more sense but the class remained invisible to the shipping module for some unknown reason... must have done something wrong).

For this a change to catalog/checkout_shipping.php is needed.

Lines 61-62

**AFTER**

  $total_weight = $cart->show_weight();
  $total_count = $cart->count_contents();

**ADD**

// BOF changes for adding class packing
  if (defined('SHIPPING_DIMENSIONS_SUPPORT') && SHIPPING_DIMENSIONS_SUPPORT == 'Ready-to-ship only') {
    $dimensions_support = 1;
  } elseif (defined('SHIPPING_DIMENSIONS_SUPPORT') && SHIPPING_DIMENSIONS_SUPPORT == 'With product dimensions') {
    $dimensions_support = 2;
  } else {
    $dimensions_support = 0;
  }
  
  if ($dimensions_support > 0) {
    require(DIR_WS_CLASSES . 'packing.php');
    $packing = new packing;
  }
// EOF changes for adding class packing

--------
Step 10
--------

Go to Admin->Tools->Packaging and add at least one shipping box (New Package)
===========================================================

You're done. Enabling dimensional support now is done in the admin of the shop: Admin->Configuration->Shipping/Packaging.

If you want to use full dimensional support you will have to enter correct dimensions for all your products (instead of the default 12 x 12 x 12).

When you want to check how the packaging algorithm packs shipments proceed with the instructions mentioned in store_shipping_boxes_used_instructions.txt.

If you want to split a product in several items for packaging (only useful in combination with ready-to-ship or full dimensional support) you will need to first make the changes described above and then continue with the changes in split_products_instructions.txt.
