Index: CHANGELOG.txt
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/uc_edi/CHANGELOG.txt,v
retrieving revision 1.7
diff -u -p -r1.7 CHANGELOG.txt
--- CHANGELOG.txt	8 Nov 2010 18:56:53 -0000	1.7
+++ CHANGELOG.txt	24 Nov 2010 16:30:55 -0000
@@ -15,3 +15,5 @@ task #965738 by AntoineSolutions: Moved 
   uc_edi.admin.inc file.
 task #965778 by AntoineSolutions: Added a store status specifying whether
   Ubercart EDI is configured correctly or not.
+task #957384 by AntoineSolutions: Added token hooks for replacement tokens and
+  replaced pattern variables with template files.
Index: README.txt
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/uc_edi/README.txt,v
retrieving revision 1.3
diff -u -p -r1.3 README.txt
--- README.txt	5 Nov 2010 22:13:50 -0000	1.3
+++ README.txt	24 Nov 2010 16:30:55 -0000
@@ -38,7 +38,7 @@ To submit bug reports and feature sugges
   - Update an order after export predicate:
     admin/store/ca/uc_edi_order_export_update/edit
   - Ubercart EDI settings page: admin/store/settings/edi
-  - Manual import/export form: admin/store/orders/edi
+  - Manual import/export form: admin/store/edi
   - Ubercart EDI permissions: admin/user/permissions#module-uc_edi
 
 * Export Configuration
@@ -87,6 +87,41 @@ To submit bug reports and feature sugges
       To export orders manually:
       - Use the "Manual import/export form".
 
+  - Template files
+    EDI templates use Drupal's theme system to allow complete control over
+    exported content. To customize exported content you can either override
+    existing templates or define new templates.
+
+    - Override existing templates
+      Templates can be overridden using normal Drupal themeing practices, i.e.
+      copy uc_edi-default.tpl.php into your theme directory. If multiple themes
+      are used, the template file must be copied into each themes directory.
+      *Due to the way the Drupal theme layer works, the dummy file
+      (uc_edi.tpl.php) must also be copied.*
+
+    - Create new templates
+      New templates can be defined by implementing hook_uc_edi_templates(). An
+      array of template names should be returned.
+
+    - Preprocess functions
+      Both methods above can take advantage of the hook_preprocess_uc_edi()
+      function which lets you pass any data to all EDI templates as a variable.
+      The default template_preprocess_uc_edi() function adds all correctly
+      defined order tokens to the template files as variables. For a list of
+      order token variables spcifically added by Ubercart EDI, see the
+      documentation block at the top of the default template file.
+
+    - Testing
+      To easily test new template files, an "export" tab has been added to the
+      order view page. This new tab allows you to select any defined template
+      and automatically updates the output to that of the newly selected
+      template. A save button allows you to save an export file for testing the
+      import process of your fulfillment houses.
+      *This order export form does not update the order status and should only
+      be used for testing template files or viewing and saving orders that have
+      already been processed. For manually exporting orders, the Manual
+      import/export form should be used.*
+
   - Update an order after export
     Ubercart EDI leverages "Conditional Actions" for updating orders after a
     successful export. The Ubercart EDI module creates an "Order has been
Index: uc_edi.admin.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/uc_edi/uc_edi.admin.inc,v
retrieving revision 1.1
diff -u -p -r1.1 uc_edi.admin.inc
--- uc_edi.admin.inc	8 Nov 2010 18:09:26 -0000	1.1
+++ uc_edi.admin.inc	24 Nov 2010 16:30:55 -0000
@@ -179,28 +179,12 @@ function uc_edi_settings_form() {
     ),
   );
   $form['export']['orders']['uc_edi_order_export_method_sub'] = uc_edi_order_export_method();
-  $form['export']['orders']['uc_edi_order_export_pattern'] = array(
-    '#type' => 'textarea',
-    '#title' => t('Order export pattern'),
-    '#description' => t('This is the pattern used for each order in the export file.<br />Separate values by commas and use any order field keyword, i.e. !delivery_first_name.<br />Use: !_ to add a line break, !products to add the products, !delivery_name, !billing_name.<br />Add (#) to any key where # is the length you want the value to be.'),
-    '#default_value' => variable_get('uc_edi_order_export_pattern', '01, !billing_name(29), !order_id(6), !uid(10), !_, 02, !delivery_name(30), !delivery_street1(20), !delivery_street2(20), !_, 03, !delivery_city(15), !delivery_zone_code(2), !delivery_country_name(10), !delivery_postal_code(13), !_, 04, !delivery_phone(15), !spacer(15), !primary_email(41), !_, !products, 10, !billing_name(30), !billing_street1(30), !_, 11, !billing_street2(30), !billing_city(15), !billing_zone_code(2), !billing_country_name(20), !billing_postal_code(13), !_, 12, !billing_phone(15), !billing_first_name(15), !primary_email(41)'),
-  );
-  $form['export']['orders']['uc_edi_order_export_product_pattern'] = array(
-    '#type' => 'textarea',
-    '#title' => t('Order product export pattern'),
-    '#description' => t('This is the pattern used for each order product in the export file.<br />Separate values by commas and use any product field keyword, i.e. !qty.<br />Use: !_ to add a line break, !# for the incremented order line number.'),
-    '#default_value' => variable_get('uc_edi_order_export_product_pattern', '05, !#(3), !spacer(3), !qty(11), EA, !price(16), !spacer(4), !model(15)'),
-  );
-  $form['export']['orders']['uc_edi_order_export_delimiter'] = array(
+  $form['export']['orders']['uc_edi_order_export_template'] = array(
     '#type' => 'select',
-    '#title' => t('Export pattern delimiter'),
-    '#options' => array(
-      '' => t('none'),
-      ',' => t('comma (,)'),
-      '|' => t('pipe (|)'),
-      '\t' => t('tab (\t)'),
-    ),
-    '#default_value' => variable_get('uc_edi_order_export_delimiter', '\t'),
+    '#title' => t('Export template'),
+    '#description' => t('Select the export template to use.<br />This is separate from the template used when exporting on checkout completion which is configured through <a href="!url">Conditional actions</a>.', array('!url' => url(CA_UI_PATH))),
+    '#options' => uc_edi_template_options(),
+    '#default_value' => variable_get('uc_edi_order_export_template', 'default'),
   );
   $form['export']['orders']['uc_edi_order_export_next_batch_id'] = array(
     '#type' => 'textfield',
@@ -291,7 +275,7 @@ function uc_edi_order_export_method() {
     case 'manual':
       // Display help message to user.
       $form['message'] = array(
-        '#value' => t('Exporting orders manually can be accomplished using the <a href="!url">Manual EDI import/export form</a>.', array('!url' => url('admin/store/orders/edi'))),
+        '#value' => t('Exporting orders manually can be accomplished using the <a href="!url">Manual EDI import/export form</a>.', array('!url' => url('admin/store/edi'))),
       );
       // Hide unused form items.
       $form['uc_edi_order_export_freq']['#type'] = 'hidden';
@@ -513,11 +497,26 @@ function uc_edi_manual_form() {
   // Retrieve orders ready for export from database.
   $result = db_query("SELECT order_id FROM {uc_orders} WHERE order_status = '%s' ORDER BY order_id", variable_get('uc_edi_order_export_status', 'edi_export'));
   $orders = array();
-  // Build export preview and keep count of orders.
-  while ($order = db_fetch_object($result)) {
+  $count = db_affected_rows();
+  $header = TRUE;
+  $footer = FALSE;
+
+  // Build the export preview.
+  for ($i = 0; $order = db_fetch_object($result); ++$i) {
     $orders[] = $order->order_id;
+    // Load the order object.
     $order = uc_order_load($order->order_id);
-    $preview .= uc_edi_generate_order_export($order);
+
+    if ($i + 1 == $count) {
+      $footer = TRUE;
+    }
+
+    // Create the order export string.
+    $preview .= theme('uc_edi', $order, variable_get('uc_edi_order_export_template', 'default'), $header, $footer);
+
+    if ($header) {
+      $header = FALSE;
+    }
   }
 
   $form['export'] = array(
@@ -527,8 +526,6 @@ function uc_edi_manual_form() {
     '#collapsed' => FALSE,
   );
 
-  // Number of orders ready for export.
-  $count = count($orders);
   switch (variable_get('uc_edi_order_export_method', 'cron')) {
     case 'checkout':
       $message = t('Orders are configured to export on checkout completion. !count order(s) require manual export.', array('!count' => $count));
@@ -559,11 +556,11 @@ function uc_edi_manual_form() {
     '#collapsed' => TRUE,
   );
 
-  if (count($orders) == 0) {
+  if ($count == 0) {
     $preview = t('No orders awaiting export.');
   }
   elseif (empty($preview)) {
-    $preview = t('No output was generated. Check your order and product export patterns on the <a href="!url">Ubercart EDI settings</a> page.', array('!url' => url('admin/store/settings/edi')));
+    $preview = t('No output was generated. Check your template files.');
   }
   $form['export']['export_preview']['contents'] = array(
     '#value' => '<div><pre>' . $preview . '</pre></div>',
@@ -710,3 +707,110 @@ function uc_edi_order_export_archive_fil
 
   drupal_goto('admin/store/edi/download');
 }
+
+/**
+ * Generates a manual order export form for individual orders.
+ */
+function uc_edi_order_export_form($form_state, $order) {
+  drupal_set_message(t('This order export form does not update the order status and should only be used for testing template files or viewing and saving already exported orders. To manually download export files, the <a href="!url">Manual import/export form</a> should be used.', array('!url' => url('admin/store/edi'))), 'warning');
+
+  $template = variable_get('uc_edi_order_export_template', 'default');
+  $form['template'] = array(
+    '#type' => 'select',
+    '#title' => t('Template'),
+    '#description' => t('Select the export template to use.'),
+    '#options' => uc_edi_template_options(),
+    '#default_value' => $template,
+    '#ahah' => array(
+      'method' => 'replace',
+      'path' => 'admin/store/orders/' . $order->order_id . '/export/template-js',
+      'wrapper' => 'template-sub-wrapper',
+    ),
+  );
+  $form['export'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Export'),
+    '#collapsible' => FALSE,
+    '#collapsed' => FALSE,
+  );
+  $form['export']['template_sub'] = uc_edi_order_export_template($order, $template);
+  $form['save'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+/**
+ * Returns the unrendered order export form item.
+ *
+ * @param $order
+ *   A reference to an order object to export.
+ * @param $template
+ *   A reference to an order export template name.
+ *
+ *  @return
+ *    An array containing the unrendered form item.
+ */
+function uc_edi_order_export_template(&$order, &$template) {
+  // Create the order export string.
+  $form = array(
+    '#value' => theme('uc_edi', $order, $template, TRUE, TRUE),
+  );
+
+  // Add prefix and suffix elements.
+  $form += array(
+    '#prefix' => '<pre id="template-sub-wrapper">',
+    '#suffix' => '</pre>',
+  );
+
+  return $form;
+}
+
+/**
+ * Returns the rendered order export form item.
+ *
+ * @param $order
+ *   A reference to an order object to export.
+ */
+function uc_edi_order_export_template_js(&$order) {
+  // Retrieve the template from $_POST.
+  $template = $_POST['template'];
+
+  // Retrieve the unrendered order export form item.
+  $form = uc_edi_order_export_template($order, $template);
+
+  // Remove #prefix and #suffix to avoid a duplicate wrapper.
+  unset($form['#prefix'], $form['#suffix']);
+
+  // Build and render the form item, then return it in JSON format.
+  $form_state = array();
+  $form = form_builder($form['#name'], $form, $form_state);
+  $output = drupal_render($form);
+  drupal_json(array('status' => TRUE, 'data' => $output));
+}
+
+/**
+ * Downloads an individual order export.
+ */
+function uc_edi_order_export_form_submit($form, &$form_state) {
+  $order = $form['#parameters'][2];
+
+  // Build the http headers necessary for a file download.
+  $prefix = variable_get('uc_edi_order_export_prefix', 'export-');
+  $ext = variable_get('uc_edi_order_export_extension', 'edi');
+  $headers = array(
+    'Content-Length:' . filesize($output),
+    'Content-Disposition: attachment; filename="' . $prefix . $order->order_id . '.' . $ext . '"',
+    'Content-Type: application/octet-stream'
+  );
+  foreach ($headers as $header) {
+    $header = preg_replace('/\r?\n(?!\t| )/', '', $header);
+    drupal_set_header($header);
+  }
+
+  // Output the order export.
+  print theme('uc_edi', $order, $form_state['values']['template'], TRUE, TRUE);
+  exit();
+}
Index: uc_edi.info
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/uc_edi/uc_edi.info,v
retrieving revision 1.3
diff -u -p -r1.3 uc_edi.info
--- uc_edi.info	4 Nov 2010 21:18:00 -0000	1.3
+++ uc_edi.info	24 Nov 2010 16:30:55 -0000
@@ -2,6 +2,7 @@
 name = Ubercart EDI
 description = Ubercart EDI import and export module; currently handles orders.
 dependencies[] = ca
+dependencies[] = token
 dependencies[] = uc_order
 package = Ubercart - extra
 core = 6.x
Index: uc_edi.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/uc_edi/uc_edi.install,v
retrieving revision 1.4
diff -u -p -r1.4 uc_edi.install
--- uc_edi.install	5 Nov 2010 22:13:50 -0000	1.4
+++ uc_edi.install	24 Nov 2010 16:30:55 -0000
@@ -43,9 +43,7 @@ function uc_edi_uninstall() {
   variable_del('uc_edi_order_export_mail_body');
   variable_del('uc_edi_order_export_freq');
   variable_del('uc_edi_order_export_status');
-  variable_del('uc_edi_order_export_pattern');
-  variable_del('uc_edi_order_export_product_pattern');
-  variable_del('uc_edi_order_export_delimiter');
+  variable_del('uc_edi_order_export_template');
   variable_del('uc_edi_order_export_last');
   variable_del('uc_edi_order_export_next_batch_id');
   variable_del('uc_edi_order_export_product_line_number');
@@ -183,3 +181,22 @@ function uc_edi_update_6002() {
 
   return array();
 }
+
+/**
+ * Implements hook_update_N().
+ */
+function uc_edi_update_6100() {
+  $return = array();
+
+  // Clear the menu cache.
+  menu_cache_clear_all();
+
+  // Remove deprecated vaiables
+  variable_del('uc_edi_order_export_pattern');
+  variable_del('uc_edi_order_export_product_pattern');
+  variable_del('uc_edi_order_export_delimiter');
+
+  $return[] = array('success' => TRUE, 'query' => 'Export patterns have been replaced with templates. Please create any necessary custom templates.');
+
+  return $return;
+}
Index: uc_edi.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/uc_edi/uc_edi.module,v
retrieving revision 1.8
diff -u -p -r1.8 uc_edi.module
--- uc_edi.module	8 Nov 2010 18:56:53 -0000	1.8
+++ uc_edi.module	24 Nov 2010 16:30:55 -0000
@@ -31,7 +31,7 @@ function uc_edi_help($path, $arg) {
       $output .= '<li>' . t('Export the order predicate: <a href="!url">admin/store/ca/uc_edi_order_export/edit</a>', array('!url' => url('admin/store/ca/uc_edi_order_export/edit'))) . '</li>';
       $output .= '<li>' . t('Update an order after export predicate: <a href="!url">admin/store/ca/uc_edi_order_export_update/edit</a>', array('!url' => url('admin/store/ca/uc_edi_order_export_update/edit'))) . '</li>';
       $output .= '<li>' . t('Ubercart EDI settings page: <a href="!url">admin/store/settings/edi</a>', array('!url' => url('admin/store/settings/edi'))) . '</li>';
-      $output .= '<li>' . t('Manual import/export form: <a href="!url">admin/store/orders/edi</a>', array('!url' => url('admin/store/orders/edi'))) . '</li>';
+      $output .= '<li>' . t('Manual import/export form: <a href="!url">admin/store/edi</a>', array('!url' => url('admin/store/edi'))) . '</li>';
       $output .= '<li>' . t('Ubercart EDI permissions: <a href="!url">admin/user/permissions#module-uc_edi</a>', array('!url' => url('admin/user/permissions', array('fragment' => 'module-uc_edi')))) . '</li></ul>';
       $output .= '<a name="export-configuration"><h2>' . t('Export Configuration') . '</h2></a>';
       $output .= '<h3>' . t('Directories') . '</h3>';
@@ -57,6 +57,18 @@ function uc_edi_help($path, $arg) {
       $output .= '<h4>' . t('Manually') . '</h4>';
       $output .= '<p>' . t('To export orders manually:') . '</p>';
       $output .= '<ul><li>' . t('Use the "Manual import/export form".</p>') . '</li></ul>';
+      $output .= '<a name="export-template-files"><h3>' . t('Template files') . '</h3></a>';
+      $output .= '<p>' . t("EDI templates use Drupal's theme system to allow complete control over exported content. To customize exported content you can either override existing templates or define new templates.") . '</p>';
+      $output .= '<h4>' . t('Override existing templates') . '</h4>';
+      $output .= '<p>' . t('Templates can be overridden using normal Drupal themeing practices, i.e. copy uc_edi-default.tpl.php into your theme directory. If multiple themes are used, the template file must be copied into each themes directory.') . '<br />';
+      $output .= '<strong>' . t('Due to the way the Drupal theme layer works, the dummy file (uc_edi.tpl.php) must also be copied.') . '</strong></p>';
+      $output .= '<h4>' . t('Create new templates') . '</h4>';
+      $output .= '<p>' . t('New templates can be defined by implementing hook_uc_edi_templates(). An array of template names should be returned.') . '</p>';
+      $output .= '<h4>' . t('Preprocess functions') . '</h4>';
+      $output .= '<p>' . t('Both methods above can take advantage of the hook_preprocess_uc_edi() function which lets you pass any data to all EDI templates as a variable. The default template_preprocess_uc_edi() function adds all correctly defined order tokens to the template files as variables. For a list of order token variables spcifically added by Ubercart EDI, see the documentation block at the top of the default template file.') . '</p>';
+      $output .= '<h4>' . t('Testing') . '</h4>';
+      $output .= '<p>' . t('To easily test new template files, an "export" tab has been added to the order view page. This new tab allows you to select any defined template and automatically updates the output to that of the newly selected template. A save button allows you to save an export file for testing the import process of your fulfillment houses.') . '<br />';
+      $output .= '<strong>' . t('This order export form does not update the order status and should only be used for testing template files or viewing and saving orders that have already been processed. For manually exporting orders, the <a href="!url">Manual import/export form</a> should be used.', array('!url' => url('admin/store/edi'))) . '</strong></p>';
       $output .= '<h3>' . t('Update an order after export') . '</h3>';
       $output .= '<p>' . t('Ubercart EDI leverages "Conditional Actions" for updating orders after a successful export. The Ubercart EDI module creates an "Order has been exported" trigger that is pulled when an order is successfully exported. By default, an "Update an order after export" predicate is created which is tied to the "Order has been exported" trigger that adds an order comment, an admin comment and updates the order status of orders that have been successfully exported.');
       $output .= '<a name="export-configuration"><h2>' . t('Import Configuration') . '</h2></a>';
@@ -137,6 +149,22 @@ function uc_edi_menu() {
     'type' => MENU_CALLBACK,
     'file' => 'uc_edi.admin.inc',
   );
+  $items['admin/store/orders/%uc_order/export'] = array(
+    'title' => 'Export',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('uc_edi_order_export_form', 3),
+    'access arguments' => array('administer edi orders'),
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 9,
+    'file' => 'uc_edi.admin.inc',
+  );
+  $items['admin/store/orders/%uc_order/export/template-js'] = array(
+    'page callback' => 'uc_edi_order_export_template_js',
+    'page arguments' => array(3),
+    'access arguments' => array('administer edi orders'),
+    'type' => MENU_CALLBACK,
+    'file' => 'uc_edi.admin.inc',
+  );
 
   return $items;
 }
@@ -179,6 +207,152 @@ function uc_edi_form_ca_predicate_meta_f
   }
 }
 
+/**
+ * Implements hook_theme().
+ */
+function uc_edi_theme() {
+  return array(
+    'uc_edi' => array(
+      'template' => 'uc_edi',
+      'path' => drupal_get_path('module', 'uc_edi') . '/templates',
+      'arguments' => array(
+        'order' => NULL,
+        'template' => 'default',
+        'uc_edi_header' => NULL,
+        'uc_edi_footer' => NULL,
+      ),
+    ),
+  );
+}
+
+/**
+ * Implements hook_token_values().
+ */
+function uc_edi_token_values($type, $object = NULL) {
+  $values = array();
+  switch ($type) {
+    case 'order':
+      $order = $object;
+
+      $values['order-edi-batch-id'] = variable_get('uc_edi_order_next_batch_id', 1000);
+      $values['order-payment-method'] = check_plain($order->payment_method);
+
+      $shipping_charges = 0;
+      $tax_charges = 0;
+      if (is_array($order->line_items)) {
+        // Set line item uc_price context.
+        $context = array(
+          'revision' => 'altered',
+          'type' => 'line_item',
+        );
+
+        foreach ($order->line_items as $key => $value) {
+          $context['subject'] = array(
+            'order' => $order,
+            'line_item' => $order->line_items[$key],
+          );
+
+          // Calculate the total shipping charges.
+          if ($value['type'] == 'shipping') {
+            $shipping_charges += uc_price($order->line_items[$key]['amount'], $context);
+          }
+          // Calculate the total tax charges.
+          if ($value['type'] == 'tax') {
+            $tax_charges += uc_price($order->line_items[$key]['amount'], $context);
+          }
+        }
+      }
+
+      $context = array(
+        'revision' => 'altered',
+      );
+      $values['order-shipping-charges'] = uc_price($shipping_charges, $context);
+      $values['order-tax-charges'] = uc_price($tax_charges, $context);
+
+      $context['revision'] = 'formatted';
+      $values['order-shipping-charges-formatted'] = uc_price($shipping_charges, $context);
+      $values['order-tax-charges-formatted'] = uc_price($tax_charges, $context);
+
+      $values['order-shipping-first-name'] = check_plain($order->delivery_first_name);
+      $values['order-shipping-last-name'] = check_plain($order->delivery_last_name);
+      $values['order-shipping-company'] = check_plain($order->delivery_company);
+      $values['order-shipping-street1'] = check_plain($order->delivery_street1);
+      $values['order-shipping-street2'] = check_plain($order->delivery_street2);
+      $values['order-shipping-city'] = check_plain($order->delivery_city);
+      $values['order-shipping-zone'] = ($order->delivery_zone != 0 ? check_plain($order->delivery_zone) : '');
+      $values['order-shipping-zone-code'] = uc_get_zone_code(check_plain($order->delivery_zone));
+      $values['order-shipping-postal_code'] = check_plain($order->delivery_postal_code);
+      $values['order-shipping-country'] = check_plain($order->delivery_country);
+      $values['order-shipping-country-name'] = db_result(db_query("SELECT country_name FROM {uc_countries} WHERE country_id = %d", $order->delivery_country));
+      $values['order-shipping-country-code-2'] = db_result(db_query("SELECT country_iso_code_2 FROM {uc_countries} WHERE country_id = %d", $order->delivery_country));
+      $values['order-shipping-country-code-3'] = db_result(db_query("SELECT country_iso_code_3 FROM {uc_countries} WHERE country_id = %d", $order->delivery_country));
+      $values['order-billing-first-name'] = check_plain($order->billing_first_name);
+      $values['order-billing-last-name'] = check_plain($order->billing_last_name);
+      $values['order-billing-company'] = check_plain($order->billing_company);
+      $values['order-billing-street1'] = check_plain($order->billing_street1);
+      $values['order-billing-street2'] = check_plain($order->billing_street2);
+      $values['order-billing-city'] = check_plain($order->billing_city);
+      $values['order-billing-zone'] = check_plain($order->billing_zone);
+      $values['order-billing-zone-code'] = uc_get_zone_code(check_plain($order->billing_zone));
+      $values['order-billing-postal_code'] = check_plain($order->billing_postal_code);
+      $values['order-billing-country'] = check_plain($order->billing_country);
+      $values['order-billing-country-name'] = db_result(db_query("SELECT country_name FROM {uc_countries} WHERE country_id = %d", $order->billing_country));
+      $values['order-billing-country-code-2'] = db_result(db_query("SELECT country_iso_code_2 FROM {uc_countries} WHERE country_id = %d", $order->billing_country));
+      $values['order-billing-country-code-3'] = db_result(db_query("SELECT country_iso_code_3 FROM {uc_countries} WHERE country_id = %d", $order->billing_country));
+      $values['order-date-created-unix'] = $order->created;
+      $values['order-date-modified-unix'] = $order->modified;
+      break;
+  }
+
+  return $values;
+}
+
+/**
+ * Implements hook_token_list().
+ */
+function uc_edi_token_list($type = 'all') {
+  $tokens = array();
+
+  if ($type == 'order' || $type == 'ubercart' || $type == 'all') {
+    $tokens['uc_edi']['order-edi-batch-id'] = t('The export batch number.');
+    $tokens['uc_edi']['order-payment-method'] = t('The order payment method.');
+    $tokens['uc_edi']['order-shipping-charges'] = t('The "altered" order total shipping charges.');
+    $tokens['uc_edi']['order-shipping-charges-formatted'] = t('The "formatted" order total shipping charges.');
+    $tokens['uc_edi']['order-tax-charges'] = t('The "altered" order total tax charges.');
+    $tokens['uc_edi']['order-tax-charges-formatted'] = t('The "formatted" order total tax charges.');
+    $tokens['uc_edi']['order-shipping-first-name'] = t('The first name of the order shipping address.');
+    $tokens['uc_edi']['order-shipping-last-name'] = t('The last name of the order shipping address.');
+    $tokens['uc_edi']['order-shipping-company'] = t('The company of the order shipping address.');
+    $tokens['uc_edi']['order-shipping-street1'] = t('The street1 of the order shipping address.');
+    $tokens['uc_edi']['order-shipping-street2'] = t('The street2 of the order shipping address.');
+    $tokens['uc_edi']['order-shipping-city'] = t('The city of the order shipping address.');
+    $tokens['uc_edi']['order-shipping-zone'] = t('The zone/province/state of the order shipping address.');
+    $tokens['uc_edi']['order-shipping-zone-code'] = t('The zone/province/state code of the order shipping address.');
+    $tokens['uc_edi']['order-shipping-postal-code'] = t('The postal/zip code of the order shipping address.');
+    $tokens['uc_edi']['order-shipping-country'] = t('The country ISO 3166-1 Numeric-3 code of the order shipping address.');
+    $tokens['uc_edi']['order-shipping-country-name'] = t('The country ISO 3166-1 English short name of the order shipping address.');
+    $tokens['uc_edi']['order-shipping-country-code-2'] = t('The country ISO 3166-1 Alpha-2 code of the order shipping address.');
+    $tokens['uc_edi']['order-shipping-country-code-3'] = t('The country ISO 3166-1 Alpha-3 code of the order shipping address.');
+    $tokens['uc_edi']['order-billing-first-name'] = t('The first name of the order billing address.');
+    $tokens['uc_edi']['order-billing-last-name'] = t('The last name of the order billing address.');
+    $tokens['uc_edi']['order-billing-company'] = t('The company of the order billing address.');
+    $tokens['uc_edi']['order-billing-street1'] = t('The street1 of the order billing address.');
+    $tokens['uc_edi']['order-billing-street2'] = t('The street2 of the order billing address.');
+    $tokens['uc_edi']['order-billing-city'] = t('The city of the order billing address.');
+    $tokens['uc_edi']['order-billing-zone'] = t('The zone/province/state of the order billing address.');
+    $tokens['uc_edi']['order-billing-zone-code'] = t('The zone/province/state code of the order billing address.');
+    $tokens['uc_edi']['order-billing-postal-code'] = t('The postal/zip code of the order billing address.');
+    $tokens['uc_edi']['order-billing-country'] = t('The country ISO 3166-1 Numeric-3 code of the order billing address.');
+    $tokens['uc_edi']['order-billing-country-name'] = t('The country ISO 3166-1 English short name of the order billing address.');
+    $tokens['uc_edi']['order-billing-country-code-2'] = t('The country ISO 3166-1 Alpha-2 code of the order billing address.');
+    $tokens['uc_edi']['order-billing-country-code-3'] = t('The country ISO 3166-1 Alpha-3 code of the order billing address.');
+    $tokens['uc_edi']['order-date-created-unix'] = t('The Unix/POSIX time the order was created.');
+    $tokens['uc_edi']['order-date-modified-unix'] = t('The Unix/POSIX time the order was last modified.');
+  }
+
+  return $tokens;
+}
+
 /*******************************************************************************
  * Hook Functions (Ubercart)
  ******************************************************************************/
@@ -226,9 +400,111 @@ function uc_edi_order_state() {
   return $states;
 }
 
-/*******************************************************************************
+/**
+ * Implements hook_uc_edi_templates().
+ */
+function uc_edi_uc_edi_templates() {
+  return array('default');
+}
+
+/******************************************************************************
  * Module and Helper Functions
- ******************************************************************************/
+ *****************************************************************************/
+
+/**
+ * Preprocess a formatted export with an order's data.
+ */
+function template_preprocess_uc_edi(&$variables) {
+  $variables['products'] = $variables['order']->products;
+  if (!is_array($variables['products'])) {
+    $variables['products'] = array();
+  }
+
+  $variables['line_items'] = $variables['order']->line_items;
+  $items = _line_item_list();
+  foreach ($items as $item) {
+    if (isset($item['display_only']) && $item['display_only'] == TRUE) {
+      $result = $item['callback']('display', $variables['order']);
+      if (is_array($result)) {
+        foreach ($result as $line) {
+          $variables['line_items'][] = array(
+            'line_item_id' => $line['id'],
+            'title' => $line['title'],
+            'amount' => $line['amount'],
+            'weight' => $item['weight'],
+            'data' => $item['data'],
+          );
+        }
+      }
+    }
+  }
+  if (!is_array($variables['line_items'])) {
+    $variables['line_items'] = array();
+  }
+  usort($variables['line_items'], 'uc_weight_sort');
+
+  $types = array(
+    'order' => $variables['order'],
+  );
+
+  $full = new stdClass();
+  $full->tokens = $full->values = array();
+  foreach ($types as $type => $object) {
+    $temp = token_get_values($type, $object, FALSE, $options);
+    $full->tokens = array_merge($full->tokens, $temp->tokens);
+    $full->values = array_merge($full->values, $temp->values);
+  }
+
+  foreach ($full->tokens as $key => $token) {
+    $value = $full->values[$key];
+    $variables[str_replace('-', '_', $token)] = $value;
+  }
+
+  // Add template file suggestions.
+  $variables['template_files'] = array(
+    'uc_edi-default',
+    'uc_edi-'. $variables['template'],
+  );
+}
+
+/**
+ * Respond to EDI template definitions.
+ *
+ * This hook is invoked from uc_edi_template_list().
+ *
+ * @return
+ *   Array of EDI template names.
+ *
+ * @ingroup hooks
+ */
+function hook_uc_edi_templates() {
+  return array('default');
+}
+
+/**
+ * Return an array of EDI templates.
+ */
+function uc_edi_template_list() {
+  $templates = drupal_map_assoc(module_invoke_all('uc_edi_templates'));
+
+  // Sort the template names alphabetically.
+  sort($templates);
+
+  return $templates;
+}
+
+/**
+ * Return a list of options for a template select box.
+ */
+function uc_edi_template_options($custom = FALSE) {
+  $templates = drupal_map_assoc(uc_edi_template_list());
+
+  if ($custom) {
+    $templates[0] = t('Custom template');
+  }
+
+  return $templates;
+}
 
 /**
  * Creates an export file containing $contents and calls delievery methods.
@@ -289,248 +565,6 @@ function uc_edi_create_export($type, $fi
 }
 
 /**
- * Generates an order export string from the order export pattern.
- *
- * @param $order
- *   Object reference to an order.
- *
- * @return
- *   An order export string replacing placeholders in the order export pattern
- *   with values from the order.
- */
-function uc_edi_generate_order_export(&$order) {
-  $pattern = variable_get('uc_edi_order_export_pattern', '01, !billing_name(29), !order_id(6), !uid(10), !_, 02, !delivery_name(30), !delivery_street1(20), !delivery_street2(20), !_, 03, !delivery_city(15), !delivery_zone_code(2), !delivery_country_name(10), !delivery_postal_code(13), !_, 04, !delivery_phone(15), !spacer(15), !primary_email(41), !_, !products, 10, !billing_name(30), !billing_street1(30), !_, 11, !billing_street2(30), !billing_city(15), !billing_zone_code(2), !billing_country_name(20), !billing_postal_code(13), !_, 12, !billing_phone(15), !billing_first_name(15), !primary_email(41)');
-  $pattern = explode(',', $pattern);
-  $delimiter = variable_get('uc_edi_order_export_delimiter', '\t');
-  $delimiter = $delimiter == '\t' ? "\t" : $delimiter;
-
-  $values = uc_edi_order_values($order);
-
-  // Process the orders here, add the data to the current output string.
-  foreach ($pattern as $key) {
-    $key = trim($key);
-    if (substr($key, 0, 1) == '!' && strpos($key, '(') > 0) {
-      $match = substr($key, 0, strpos($key, '('));
-      $length = intval(trim(substr($key, strpos($key, '(')), '()'));
-    }
-    else {
-      $match = $key;
-      $length = FALSE;
-    }
-
-    if ($match == '!_') {
-      $output .= "\n";
-    }
-    elseif ($match == '!products') {
-      $output .= $values[$match];
-    }
-    elseif (!isset($values[$match])) {
-      $output .= $key . $delimiter;
-    }
-    else {
-      if ($length != FALSE) {
-        $temp = substr((string) $values[$match], 0, $length);
-        if (strlen($temp) < $length) {
-          $temp = $temp . str_repeat(' ', $length - strlen($temp));
-        }
-        $output .= $temp . $delimiter;
-      }
-      else {
-        $output .= $values[$match] . $delimiter;
-      }
-    }
-  }
-
-  if (substr($output, -1) !== "\n") {
-    $output .= "\n";
-  }
-
-  return $output;
-}
-
-/**
- * Generates a product export string from the product export pattern.
- *
- * @param $line
- *   Int reference to the line number.
- * @param $product
- *   Object reference to a product.
- *
- * @return
- *   A product export string replacing placeholders in the product export
- *   pattern with values from the product.
- */
-function uc_edi_generate_order_product_export(&$line, &$product) {
-  $pattern = variable_get('uc_edi_order_export_product_pattern', '05, !#(3), !spacer(3), !qty(11), EA, !price(16), !spacer(4), !model(15)');
-  $pattern = explode(',', $pattern);
-  $delimiter = variable_get('uc_edi_order_export_delimiter', '\t');
-  $delimiter = $delimiter == '\t' ? "\t" : $delimiter;
-
-  $values = uc_edi_order_product_values($line, $product);
-
-  // Process the orders here, add the data to the current output string.
-  foreach ($pattern as $key) {
-    $key = trim($key);
-    if (substr($key, 0, 1) == '!' && strpos($key, '(') > 0) {
-      $match = substr($key, 0, strpos($key, '('));
-      $length = intval(trim(substr($key, strpos($key, '(')), '()'));
-    }
-    else {
-      $match = $key;
-      $length = FALSE;
-    }
-
-    if ($match == '!_') {
-      $output .= "\n";
-    }
-    elseif (!isset($values[$match])) {
-      $output .= $key . $delimiter;
-    }
-    else {
-      if ($length != FALSE) {
-        $temp = substr((string) $values[$match], 0, $length);
-        if (strlen($temp) < $length) {
-          $temp = $temp . str_repeat(' ', $length - strlen($temp));
-        }
-        $output .= $temp . $delimiter;
-      }
-      else {
-        $output .= $values[$match] . $delimiter;
-      }
-    }
-  }
-
-  if (substr($output, -1) !== "\n") {
-    $output .= "\n";
-  }
-
-  return $output;
-}
-
-/**
- * Returns a list of replacement values for the order export pattern.
- *
- * @param $order
- *   Order object.
- *
- * @return
- *   Array of replacement values for order export patterns.
- */
-function uc_edi_order_values($order) {
-  static $data;
-  $order = (array) $order;
-  foreach (array_keys($order) as $key) {
-    if (!is_array($order[$key]) && !is_object($order[$key])) {
-      $values['!' . $key] = $order[$key];
-    }
-  }
-
-  // Build the products lines.
-  if (is_array($order['products'])) {
-    for ($line = 1; $product = $order['products'][$line - 1]; ++$line) {
-      $products .= uc_edi_generate_order_product_export($line, $product);
-    }
-    $values['!products'] = $products;
-  }
-
-  // This is just an incremental value arbitrarily starting at 1000.
-  // Adjust this value to have a different starting batch ID.
-  $values['!batch_id'] = variable_get('uc_edi_order_export_next_batch_id', 1000);
-  variable_set('uc_edi_order_export_next_batch_id', $values['!batch_id'] + 1);
-
-  // This is a helper value combining the first and last names.
-  $values['!delivery_name'] = $order['delivery_first_name'] . ' ' . $order['delivery_last_name'];
-  $values['!billing_name'] = $order['billing_first_name'] . ' ' . $order['billing_last_name'];
-
-  // Add the zone code and country name values.
-  if (!isset($data['zone_code'][$order['delivery_zone']])) {
-    $values['!delivery_zone_code'] = uc_get_zone_code($order['delivery_zone']);
-    $data['zone_code'][$order['delivery_zone']] = $values['!delivery_zone_code'];
-  }
-  else {
-    $values['!delivery_zone_code'] = $data['zone_code'][$order['delivery_zone']];
-  }
-  if (!isset($data['zone_code'][$order['billing_zone']])) {
-    $values['!billing_zone_code'] = uc_get_zone_code($order['billing_zone']);
-    $data['zone_code'][$order['billing_zone']] = $values['!billing_zone_code'];
-  }
-  else {
-    $values['!billing_zone_code'] = $data['zone_code'][$order['delivery_zone']];
-  }
-
-  if (!isset($data['country_name'][$order['delivery_country']])) {
-    $values['!delivery_country_name'] = db_result(db_query("SELECT country_name FROM {uc_countries} WHERE country_id = %d", $order['delivery_country']));
-    $data['country_name'][$order['delivery_country']] = $values['!delivery_country_name'];
-  }
-  else {
-    $values['!delivery_country_name'] = $data['country_name'][$order['delivery_country']];
-  }
-
-  if (!isset($data['country_name'][$order['billing_country']])) {
-    $values['!billing_country_name'] = db_result(db_query("SELECT country_name FROM {uc_countries} WHERE country_id = %d", $order['billing_country']));
-    $data['country_name'][$order['billing_country']] = $values['!billing_country_name'];
-  }
-  else {
-    $values['!billing_country_name'] = $data['country_name'][$order['billing_country']];
-  }
-
-  if (!isset($data['country_code'][$order['delivery_country']])) {
-    $values['!delivery_country_code'] = db_result(db_query("SELECT country_iso_code_3 FROM {uc_countries} WHERE country_id = %d", $order['delivery_country']));
-    $data['country_code'][$order['delivery_country']] = $values['!delivery_country_code'];
-  }
-  else {
-    $values['!delivery_country_code'] = $data['country_code'][$order['delivery_country']];
-  }
-
-  if (!isset($data['country_code'][$order['billing_country']])) {
-    $values['!billing_country_code'] = db_result(db_query("SELECT country_iso_code_3 FROM {uc_countries} WHERE country_id = %d", $order['billing_country']));
-    $data['country_code'][$order['billing_country']] = $values['!billing_country_code'];
-  }
-  else {
-    $values['!billing_country_code'] = $data['country_code'][$order['billing_country']];
-  }
-
-  $values['!spacer'] = ' ';
-
-  $price = 0;
-  foreach ($order['line_items'] as $line) {
-    if ($line['type'] == 'shipping') {
-      $price += $line['amount'];
-    }
-  }
-  $values['!uc_shipping_charges'] = (string) round($price, 2);
-
-  return $values;
-}
-
-/**
- * Returns a list of replacement values for the product export pattern.
- *
- * @param $line
- *   Int reference to the line number.
- * @param $product
- *   A product object.
- *
- * @return
- *  Array or replacement values for product export patterns.
- */
-function uc_edi_order_product_values(&$line, $product) {
-  $product = (array) $product;
-
-  foreach (array_keys($product) as $key) {
-    if (!is_array($product[$key]) && !is_object($product[$key])) {
-      $values['!' . $key] = $product[$key];
-    }
-  }
-
-  $values['!batch_id'] = variable_get('uc_edi_order_export_next_batch_id', 1000);
-  $values['!#'] = $line;
-  $values['!spacer'] = ' ';
-
-  return $values;
-}
-
-/**
  * Loads and parses all available import files and archives them when done.
  */
 function uc_edi_import_orders() {
@@ -586,7 +620,7 @@ function uc_edi_export_order(&$order) {
   // Generate the filename using the order Id for uniqueness.
   $filename = variable_get('uc_edi_order_export_prefix', 'export-') . $order->order_id . '.' . variable_get('uc_edi_order_export_extension', 'edi');
   // Create the order's export string.
-  $contents .= uc_edi_generate_order_export($order);
+  $contents .= theme('uc_edi', $order, variable_get('uc_edi_order_export_template', 'default'), TRUE, TRUE);
 
   // Create the export file.
   if (uc_edi_create_export('order', $filename, $contents)) {
@@ -615,13 +649,26 @@ function uc_edi_export_orders() {
 
   // Load all orders ready for export.
   $result = db_query("SELECT order_id FROM {uc_orders} WHERE order_status = '%s' ORDER BY order_id", variable_get('uc_edi_order_export_status', 'edi_export'));
-  while ($order = db_fetch_object($result)) {
+  $count = db_affected_rows();
+  $header = TRUE;
+  $footer = FALSE;
+
+  for ($i = 0; $order = db_fetch_object($result); ++$i) {
+    // Load the order object.
+    $order = uc_order_load($order->order_id);
     // Keep array of orders to modify after successfull export.
     $orders[] = $order;
-    // Load each valid order and create its export string.
-    $order = uc_order_load($order->order_id);
-    // Create each order's export string.
-    $contents .= uc_edi_generate_order_export($order);
+
+    if ($i + 1 == $count) {
+      $footer = TRUE;
+    }
+
+    // Create the order export string.
+    $contents .= theme('uc_edi', $order, variable_get('uc_edi_order_export_template', 'default'), $header, $footer);
+
+    if ($header) {
+      $header = FALSE;
+    }
   }
 
   // Create the export file.
Index: templates/uc_edi-default.tpl.php
===================================================================
RCS file: templates/uc_edi-default.tpl.php
diff -N templates/uc_edi-default.tpl.php
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ templates/uc_edi-default.tpl.php	24 Nov 2010 16:30:55 -0000
@@ -0,0 +1,115 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * This file is the default EDI export template.
+ *
+ * Available variables:
+ * - $uc_edi_header: Is TRUE if this is the first order in the export (header
+ *   should be printed if desired), otherwise FALSE.
+ * - $uc_edi_footer: Is TRUE if this is the last order in the export (footer
+ *   should be printed if desired), otherwise FALSE.
+ * - $order: The Ubercart order object.
+ * - $products: An array of product objects belonging to the order.
+ * - $line_items: An array of line items belonging to the order.
+ *
+ * Variables added by Ubercart EDI via the Token module.
+ * - $order_edi_batch_id: The export batch number.
+ * - $order_payment_method: The order payment method.
+ * - $order_shipping_charges: The "altered" order shipping charges.
+ * - $order_shipping_charges_formatted: The "formatted" order shipping charges.
+ * - $order-tax-charges: The "altered" order total tax charges.
+ * - $order-tax-charges-formatted: The "formatted" order total tax charges.
+ * - $order_shipping_first_name: The first name of the order shipping address.
+ * - $order_shipping_last_name: The last name of the order shipping address.
+ * - $order_shipping_company: The company of the order shipping address.
+ * - $order_shipping_street1: The street1 of the order shipping address.
+ * - $order_shipping_street2: The street2 of the order shipping address.
+ * - $order_shipping_city: The city of the order shipping address.
+ * - $order_shipping_zone: The zone/province/state of the order shipping address.
+ * - $order_shipping_zone_code: The zone/province/state code of the order shipping address.
+ * - $order_shipping_postal_code: The postal/zip code of the order shipping address.
+ * - $order_shipping_country: The country ISO 3166-1 Numeric-3 code of the order shipping address.
+ * - $order_shipping_country_name: The country ISO 3166-1 English short name of the order shipping address.
+ * - $order_shipping_country_code_2: The country ISO 3166-1 Alpha-2 code of the order shipping address.
+ * - $order_shipping_country_code_3: The country ISO 3166-1 Alpha-3 code of the order shipping address.
+ * - $order_billing_first_name: The first name of the order billing address.
+ * - $order_billing_last_name: The last name of the order billing address.
+ * - $order_billing_company: The company of the order billing address.
+ * - $order_billing_street1: The street1 of the order billing address.
+ * - $order_billing_street2: The street2 of the order billing address.
+ * - $order_billing_city: The city of the order billing address.
+ * - $order_billing_zone: The zone/province/state of the order billing address.
+ * - $order_billing_zone_code: The zone/province/state code of the order billing address.
+ * - $order_billing_postal_code: The postal/zip code of the order billing address.
+ * - $order_billing_country: The country ISO 3166-1 Numeric-3 code of the order billing address.
+ * - $order_billing_country_name: The country ISO 3166-1 English short name of the order billing address.
+ * - $order_billing_country_code_2: The country ISO 3166-1 Alpha-2 code of the order billing address.
+ * - $order_billing_country_code_3: The country ISO 3166-1 Alpha-3 code of the order billing address.
+ * - $order_date_created_unix: The Unix/POSIX time the order was created.
+ * - $order_date_modified_unix: The Unix/POSIX time the order was last modified.
+ *
+ * @see template_preprocess_uc_edi()
+ * @see uc_price()
+ *
+ * @ingroup themeable
+ */
+
+// Set the delimiter.
+$delimiter = "\t";
+
+// Set the headers.
+$headers = array(
+  'customer_account_number',
+  'customer_name',
+  'order_number',
+  'order_date',
+  'sku',
+  'quantity',
+  'price',
+);
+
+if ($uc_edi_header) {
+  /* -- Delete this line to print a list of available variables and their values.
+  echo check_plain(print_r($variables, 1));
+  // */
+
+  // Print the header.
+  echo implode($delimiter, $headers) . "\n";
+}
+
+// Set the default price context.
+$context = array(
+  'revision' => 'altered',
+  'type' => 'order_product',
+);
+
+// Loop through all products printing all order and product values.
+for ($line = 0; $product = $products[$line]; ++$line) {
+  // Set the product specific price context.
+  $price_info = array(
+    'price' => $product->price,
+    'qty' => $product->qty,
+  );
+  $context['subject'] = array(
+    'order_product' => $product,
+  );
+
+  // Print the values.
+  $values = array(
+    $order_uid,
+    $order_billing_first_name . ' ' . $order_billing_last_name,
+    $order_id,
+    $order_date_created_unix,
+    $product->model,
+    $product->qty,
+    uc_price($price_info, $context),
+  );
+  echo implode($delimiter, $values) . "\n";
+}
+
+// Re-print headers as footers.
+if ($uc_edi_footer) {
+  echo implode($delimiter, $headers) . "\n";
+}
Index: templates/uc_edi.tpl.php
===================================================================
RCS file: templates/uc_edi.tpl.php
diff -N templates/uc_edi.tpl.php
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ templates/uc_edi.tpl.php	24 Nov 2010 16:30:55 -0000
@@ -0,0 +1,11 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * This file is a dummy template.
+ *
+ * It serves as a reminder to theme authors that they must have a
+ * uc_edi.tpl.php file in their theme if they want to override the other EDI
+ * templates.
+ */
