? .DS_Store
? 686362-3-rounding.diff
Index: storminvoice/storminvoice.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/storm/storminvoice/storminvoice.module,v
retrieving revision 1.4.4.72
diff -u -p -r1.4.4.72 storminvoice.module
--- storminvoice/storminvoice.module	18 Jan 2010 22:01:41 -0000	1.4.4.72
+++ storminvoice/storminvoice.module	2 Apr 2010 20:46:54 -0000
@@ -643,6 +643,10 @@ function storminvoice_insert($node) {
   _storminvoice_aftersave($node);
 }
 
+function storminvoice_validate($node, &$form) {
+  storminvoice_rounding_get('validate', array('node' => $node));
+}
+
 function storminvoice_update($node) {
   _storminvoice_beforesave($node);
 
@@ -744,19 +748,7 @@ function _storminvoice_insert_items($nod
 
 function _storminvoice_aftersave($node) {
   // Updates totals
-  $s = "SELECT SUM(amount) tamount, SUM(tax1) ttax1, SUM(tax2) ttax2, SUM(total) ttotal 
-    FROM {storminvoice_items} WHERE invoice_vid=%d";
-  $r = db_query($s, $node->vid);
-  $t = db_fetch_object($r);
-  
-  $node->amount = $t->tamount;
-  $node->tax1 = $t->ttax1;
-  $node->tax2 = $t->ttax2;
-  $node->total = $t->ttotal;
-  
-  db_query("UPDATE {storminvoice} SET
-    amount=%f, tax1=%f, tax2=%f, total=%f, totalcustomercurr=%f WHERE vid = %d",
-    $node->amount, $node->tax1, $node->tax2, $node->total, $node->totalcustomercurr, $node->vid);
+  storminvoice_rounding_get('aftersave', array('node' => $node));
 }
 
 function storminvoice_nodeapi(&$node, $op, $teaser, $page) {
@@ -846,6 +838,16 @@ function storminvoice_admin_settings() {
     '#size' => 50,
   );
   
+  $rounding_mode_list = storminvoice_rounding_list();
+  
+  $form['storminvoice_rounding_mode'] = array(
+    '#type' => 'select',
+    '#options' => $rounding_mode_list,
+    '#title' => t('Rounding Mode'),
+    '#default_value' => variable_get('storminvoice_rounding_mode', 'default'),
+    '#description' => t('You can use different rounding modes depending on your locale. Programmers can implement hook_storminvoice_rounding_mode to make new rounding modes.') . t('You are currently using "@t", described as: "@d"', array('@t' => $rounding_mode_list[variable_get('storminvoice_rounding_mode', 'default')], '@d' => storminvoice_rounding_get('desc'))),
+  );
+  
   return system_settings_form($form);
 }
 
@@ -868,3 +870,136 @@ function storminvoice_getitems($invoice_
   }
   return $items;
 }
+
+function storminvoice_rounding_mode() {
+  $list = storminvoice_rounding_list();
+
+  $return = variable_get('storminvoice_rounding_mode', 'default');
+  
+  if(key_exists($return, $list)) {
+    return $return;
+  } else {
+    return 'default'; /* In case the rounding mode is temporarily unavailable, return default. */
+  }
+}
+
+function storminvoice_rounding_list() {
+  static $list;
+  
+  if(!$list) {
+    $list = module_invoke_all('storminvoice_rounding_mode', 'list');
+  }
+  
+  return $list;
+}
+
+function storminvoice_rounding_get($info, $args = array()) {
+  $array = module_invoke_all('storminvoice_rounding_mode', $info, array_merge(array('mode' => storminvoice_rounding_mode()), $args));
+  
+  return $array[0];
+}
+
+// Rounding mechanism hook
+function storminvoice_storminvoice_rounding_mode($op, $args = array()) {
+  switch ($op) {
+    case 'list':
+      return array(
+        'default' => t('Round to 2 decimal points just before output.'), 
+        'storminvoice_recalc_tax_at_end' => t('Round pretax total, then apply tax rate (requires unified taxation rate).'),
+      );
+      break;
+    case 'desc':
+      switch($args['mode']) {
+        case 'default':
+          return t('This is the default (legacy) Storm method of calculating totals for invoices. Essentially, no rounding is done, except when displaying the totals to the user. This can lead to base amounts and taxations amounts not adding up in certain cases.');
+          break;
+        case 'storminvoice_recalc_tax_at_end':
+          return t('This method requires all your invoice lines to have exactly the same taxation scheme. The total amount before taxes is calculated, rounded, then each first tax is applied to the previous rounded amounts and rounded itself. The grand total is the total of rounded amounts.');
+          break;
+      }
+    case 'validate':
+      switch($args['mode']) {
+        case 'default':
+          break;
+        case 'storminvoice_recalc_tax_at_end':
+          $node = $args['node'];
+          foreach(array('tax1app', 'tax1percent', 'tax2app', 'tax2percent') as $property) {
+            $i = 0;
+            while (++$i) {
+              $base_attribute = 'items_' . 0 . '_' . $property;
+              $attribute = 'items_' . $i . '_' . $property;
+              $item_desc = 'items_' . $i . '_description';
+        
+              if ((!isset($node->$attribute)) || (!$node->$item_desc)) {
+                break;
+              } 
+        
+              if ($node->$attribute != $node->$base_attribute) {
+                form_set_error($attribute, t('The rounding mechanism you are using requires all lines to have the same taxation scheme. A difference was detected in that item 1\'s ' . $property . ' is ' . $node->$base_attribute . ' but item ' . ($i+1) . '\'s ' . $property . ' is ' . $node->$attribute . '.'));
+              } 
+            }
+          }
+          break;
+        default:
+          break;
+      }
+    case 'aftersave':
+      $node = $args['node'];
+      switch($args['mode']) {
+        case 'default':
+          $s = "SELECT SUM(amount) tamount, SUM(tax1) ttax1, SUM(tax2) ttax2, SUM(total) ttotal 
+            FROM {storminvoice_items} WHERE invoice_vid=%d";
+          $r = db_query($s, $node->vid);
+          $t = db_fetch_object($r);
+          
+          $node->amount = $t->tamount;
+          $node->tax1 = $t->ttax1;
+          $node->tax2 = $t->ttax2;
+          $node->total = $t->ttotal;
+          
+          db_query("UPDATE {storminvoice} SET
+            amount=%f, tax1=%f, tax2=%f, total=%f, totalcustomercurr=%f WHERE vid = %d",
+            $node->amount, $node->tax1, $node->tax2, $node->total, $node->totalcustomercurr, $node->vid);
+          break;
+        case 'storminvoice_recalc_tax_at_end':
+          $s = "SELECT SUM(amount) tamount, SUM(tax1) ttax1, SUM(tax2) ttax2, SUM(total) ttotal 
+            FROM {storminvoice_items} WHERE invoice_vid=%d";
+          $r = db_query($s, $node->vid);
+          $t = db_fetch_object($r);
+          $taxes_s = "SELECT tax1app, tax1percent, tax2app, tax2percent 
+            FROM {storminvoice_items} WHERE invoice_vid=%d";
+          $taxes_r = db_query($taxes_s, $node->vid);
+          $taxes_t = db_fetch_object($taxes_r);
+          
+          $node->amount = round($t->tamount, 2);
+          if ($taxes_t->tax1app) {
+            $node->tax1 = round($node->amount * $taxes_t->tax1percent / 100, 2);
+          } else {
+            $node->tax1 = round(0, 2);
+          }
+          switch ($taxes_t->tax2app) {
+            case 1:
+              $node->tax2 = round($node->amount * $taxes_t->tax2percent / 100, 2);
+              break;
+            case 2:
+              $node->tax2 = round(($node->amount + $node->tax1) * $taxes_t->tax2percent / 100, 2);
+              break;
+            default:
+              $node->tax2 = round(0, 2);
+              break;
+          }
+          $node->total = $node->amount + $node->tax1 + $node->tax2;
+          
+          db_query("UPDATE {storminvoice} SET
+            amount=%f, tax1=%f, tax2=%f, total=%f, totalcustomercurr=%f WHERE vid = %d",
+            $node->amount, $node->tax1, $node->tax2, $node->total, $node->totalcustomercurr, $node->vid);
+          break;
+          break;
+        default:
+          break;
+      }
+  }
+}
+
+
+
Index: storminvoice/storminvoice.test
===================================================================
RCS file: storminvoice/storminvoice.test
diff -N storminvoice/storminvoice.test
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ storminvoice/storminvoice.test	2 Apr 2010 20:46:54 -0000
@@ -0,0 +1,121 @@
+<?php
+class StorminvoiceTestCase extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => t('Storm Invoice Functionality'),
+      'description' => t('Test the functionality of the Storm 
+Invoice module'),
+      'group' => 'Storm',
+    );
+  }
+
+  public function setUp() {
+    parent::setUp('storm', 'stormattribute', 'stormorganization', 'stormproject', 'storminvoice');
+    $privileged_user = $this->drupalCreateUser(array('Storm organization: add', 'Storm invoice: add', 'Storm invoice: edit all', 'Storm invoice: view all', 'Storm project: add', 'Storm organization: view all', 'Storm: access administration pages', 'Storm: access dashboard'));
+    $this->drupalLogin($privileged_user);
+  }
+
+  public function _testStorminvoiceCreate($subtotal, $tax1, $tax2, $total, $message) {
+    $org = array(
+      'title' => $this->randomName(32),
+      'body' => $this->randomName(64),
+    );
+    $inv = array(
+      'title' => $this->randomName(32),
+      'organization_nid' => '1',
+      'items_0_description' => $this->randomName(32),
+      'items_0_amount' => '.28',
+      'items_0_tax1app' => '1',
+      'items_0_tax1percent' => '5',
+      'items_0_tax2app' => '2',
+      'items_0_tax2percent' => '7.5',
+    );
+    $this->drupalPost('node/add/stormorganization', $org, t('Save'));
+    $this->drupalPost('node/add/storminvoice', $inv, t('Save'));
+
+    $this->assertRaw('</div><div class="stormfields"><div class="amount"><div class="label"><span class="label">Amount:&nbsp;</span></div><div class="value">' . $subtotal . '&nbsp;</div></div><div class="tax"><div class="label"><span class="label">Tax 1:&nbsp;</span></div><div class="value">' . $tax1 . '&nbsp;</div></div><div class="tax"><div class="label"><span class="label">Tax 2:&nbsp;</span></div><div class="value">' . $tax2 . '&nbsp;</div></div><div class="total"><div class="label"><span class="label">Total:&nbsp;</span></div><div class="value">' . $total . '&nbsp;</div></div></div>', $message);
+  }
+
+  public function testStorminvoiceCreateDefault() {
+    $this->_testStorminvoiceCreate('0.28', '0.01', '0.02', '0.32', t('Using standard storm calculation, expecting an invoice total before tax of .28 with a first tax rate of 5% and a second compounded tax rate of 7.5% to give totals of .28 + 0.014 + 0.02205 = 0.31605, rounded to .28 + .01 + .02 = .32'));
+  }
+
+  protected function _selectRoundingMode($mode) {
+    $edit = array(
+      'storminvoice_rounding_mode' => $mode,
+    );
+    $this->drupalPost('admin/settings/storm/invoice', $edit, t('Save configuration'));
+  }
+
+  public function testStorminvoiceCreateRounded() {
+    $this->_selectRoundingMode('storminvoice_recalc_tax_at_end');
+
+    $this->_testStorminvoiceCreate('0.28', '0.01', '0.02', '0.31', t('Using rounded storm calculation, expecting an invoice total before tax of .28 with a first tax rate of 5% and a second compounded tax rate of 7.5% to give totals of .28 + 0.01 + 0.02 + 0.31, rounded at each step'));
+  }
+
+  public function testStorminvoiceCreateRoundedErr() {
+    $this->_selectRoundingMode('storminvoice_recalc_tax_at_end');
+
+    $org = array(
+      'title' => $this->randomName(32),
+      'body' => $this->randomName(64),
+    );
+    $inv = array(
+      'title' => $this->randomName(32),
+      'organization_nid' => '1',
+      'items_0_description' => $this->randomName(32),
+      'items_0_amount' => '.14',
+      'items_0_tax1app' => '1',
+      'items_0_tax1percent' => '5',
+      'items_0_tax2app' => '2',
+      'items_0_tax2percent' => '7.5',
+      'items_1_description' => $this->randomName(32),
+      'items_1_amount' => '.14',
+      'items_1_tax1app' => '1',
+      'items_1_tax1percent' => '6',
+      'items_1_tax2app' => '2',
+      'items_1_tax2percent' => '7.5',
+    );
+    $this->drupalPost('node/add/stormorganization', $org, t('Save'));
+    $this->drupalPost('node/add/storminvoice', $inv, t('Save'));
+
+    $this->assertRaw(t('The rounding mechanism you are using requires all lines to have the same taxation scheme'), t('An error should be generated if, using the recalc tax at end rounding scheme, not all lines use the same taxation scheme.'));
+  }
+  
+  public function testStorminvoiceCreateRoundedNoOutputErr() {
+    $this->_selectRoundingMode('default');
+
+    $org = array(
+      'title' => $this->randomName(32),
+      'body' => $this->randomName(64),
+    );
+    $inv = array(
+      'title' => $this->randomName(32),
+      'organization_nid' => '1',
+      'items_0_description' => $this->randomName(32),
+      'items_0_amount' => '.14',
+      'items_0_tax1app' => '1',
+      'items_0_tax1percent' => '5',
+      'items_0_tax2app' => '2',
+      'items_0_tax2percent' => '7.5',
+      'items_0_amount' => '.14',
+      'items_0_tax1app' => '1',
+      'items_0_tax1percent' => '6',
+      'items_0_tax2app' => '2',
+      'items_0_tax2percent' => '7.5',
+    );
+    $this->drupalPost('node/add/stormorganization', $org, t('Save'));
+    $this->drupalPost('node/add/storminvoice', $inv, t('Save'));
+
+    $this->assertText(t('Invoice @a has been created', array('@a' => $inv['title'])), t('Using the default rounding scheme, it is possible to create an invoice where each line has different taxation schemes. Looking for the text ' . t('Invoice @a has been created', array('@a' => $inv['title']))));
+
+    $this->_selectRoundingMode('storminvoice_recalc_tax_at_end');
+  
+    $this->drupalGet('node/2');
+  
+    /* TODO
+    
+    $this->assertText(t('The current rounding scheme is incompatible with there being different taxation rates on some lines. Note that only the first line was used in calculating the tax for the global amount.'), t('Using the recalc tax at end rounding scheme, an error is generated if attempting to view an invoice with different tax rates per line.')); */
+  }
+}
