Fixed stock quantity checks to block transactions which would result in negative...
authorJanusz Dobrowolski <janusz@frontaccounting.eu>
Sat, 20 Sep 2014 22:09:59 +0000 (00:09 +0200)
committerJanusz Dobrowolski <janusz@frontaccounting.eu>
Sun, 21 Sep 2014 20:01:38 +0000 (22:01 +0200)
18 files changed:
gl/includes/db/gl_db_bank_trans.inc
includes/db/inventory_db.inc
includes/db/manufacturing_db.inc
includes/db/sql_functions.inc
includes/ui/items_cart.inc
inventory/adjustments.php
inventory/includes/item_adjustments_ui.inc
inventory/includes/stock_transfers_ui.inc
inventory/transfers.php
manufacturing/includes/work_order_issue_ui.inc
manufacturing/work_order_add_finished.php
manufacturing/work_order_entry.php
manufacturing/work_order_issue.php
purchasing/supplier_credit.php
sales/customer_delivery.php
sales/includes/cart_class.inc
sales/includes/ui/sales_order_ui.inc
sales/sales_order_entry.php

index f88801526b63b82531213861d15b60ebe8098eb5..62900fc3ddeaac1bea4004b40047526eb3f406a9 100644 (file)
@@ -177,8 +177,8 @@ function check_bank_account_history($delta_amount, $bank_account, $date=null, $u
        if (!isset($balance) && isset($date))
                return null;    // unlimited account
 
-       if ($balance < -$delta_amount)
-               return array('amount' => $balance, 'trans_date'=> $date);
+       if (floatcmp($balance, -$delta_amount) < 0)
+               return array('amount' => $balance - $delta_amount, 'trans_date'=> $date);
 
        $balance += $delta_amount;
 
index e8f8e3c28888c4cc07b0c3d8d91e9eb4dd6742ca..96d7b8acfca864eb85efee533b1092ca03c507b6 100644 (file)
@@ -9,46 +9,77 @@
     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
     See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
 ***********************************************************************/
-function get_qoh_on_date($stock_id, $location=null, $date_=null, $exclude=0)
+function get_qoh_on_date($stock_id, $location=null, $date_=null)
 {
     if ($date_ == null)
-    {
-        $sql = "SELECT SUM(qty) FROM ".TB_PREF."stock_moves
-            WHERE stock_id=".db_escape($stock_id);
         $date_ = Today();
-        $date = date2sql($date_);
-    }
-    else
-    {
-        $date = date2sql($date_);
-        $sql = "SELECT SUM(qty) FROM ".TB_PREF."stock_moves
+
+    $date = date2sql($date_);
+
+    $sql = "SELECT SUM(qty) FROM ".TB_PREF."stock_moves
             WHERE stock_id=".db_escape($stock_id)."
             AND tran_date <= '$date'"; 
-    }
-        
+
     if ($location != null)
         $sql .= " AND loc_code = ".db_escape($location);
 
-    $result = db_query($sql, "QOH calulcation failed");
+    $result = db_query($sql, "QOH calculation failed");
 
     $myrow = db_fetch_row($result);
-    if ($exclude > 0)
-    {
-        $sql = "SELECT SUM(qty) FROM ".TB_PREF."stock_moves
-            WHERE stock_id=".db_escape($stock_id)
-            ." AND type=".db_escape($exclude)
-            ." AND tran_date = '$date'";
-
-        $result = db_query($sql, "QOH calulcation failed");
-        $myrow2 = db_fetch_row($result);
-        if ($myrow2 !== false)
-            $myrow[0] -= $myrow2[0];
-    }
 
     $qoh =  $myrow[0];
                return $qoh ? $qoh : 0;
 }
 
+/**
+*      Check whether change in stock on date would not cause negative qoh in stock history.
+*      Returns null on success or max. available quantity with respective date otherwise.
+*   Running balance is checked on daily basis only, as we do not control time of transaction.
+*
+*      $delta_qty - tested change in stock qty at $date.
+*      $date - check date; when set to null checks all the stock history.
+**/
+
+function check_negative_stock($stock_id, $delta_qty, $location=null, $date=null)
+{
+
+       if ($delta_qty >= 0)
+                return null;   // qty increese is always safe
+
+       if (!isset($date))
+               $date = Today();
+
+       $date = date2sql($date);
+
+       // check stock status on date
+    $sql = "SELECT SUM(qty) qty, '$date' tran_date FROM ".TB_PREF."stock_moves
+            WHERE stock_id=".db_escape($stock_id)."
+            AND tran_date <= '$date'"; 
+
+    if ($location)
+        $sql .= " AND loc_code = ".db_escape($location);
+
+    $result = db_query($sql, "QOH calculation failed");
+    $qos = db_fetch_assoc($result);
+
+       // check also all stock changes after the date to avoid negative stock in future
+       $sql = TB_PREF."stock_moves WHERE stock_id=".db_escape($stock_id) . " AND tran_date > '$date'";
+
+       if ($location)
+               $sql .= " AND loc_code=".db_escape($location);
+
+       $rt = running_total_sql($sql, 'qty', 'tran_date');
+
+       $sql = "SELECT  {$qos['qty']}+total qty, tran_date FROM ($rt) stock_status ORDER by total, tran_date";
+       $history = db_query($sql, 'cannot check stock history');
+       $min_qos = db_fetch($history);
+
+       if ($min_qos && ($min_qos['qty'] < $qos['qty']))
+               $qos = $min_qos;
+
+       return  -$delta_qty > $qos['qty'] ? $qos : null;
+}
+
 //--------------------------------------------------------------------------------------
 
 function get_item_edit_info($stock_id)
index ddb93c58fd2cf33041a99497eef5fbf894ad6b2a..bfd4070468eb004d0b5ef9ec29e82411c70cc051 100644 (file)
@@ -41,7 +41,7 @@ function load_stock_levels($location)
        $sql = "SELECT stock_id, SUM(qty) FROM ".TB_PREF."stock_moves WHERE tran_date <= '$date'";
        if ($location != '') $sql .= " AND loc_code = ".db_escape($location);
        $sql .= " GROUP BY stock_id";
-       $result = db_query($sql, "QOH calulcation failed");
+       $result = db_query($sql, "QOH calculation failed");
        while ($row = db_fetch($result)) {
                $qoh_stock[$row[0]] = $row[1];
        }
index febc3af824e4f1246be0f9659a7a6aced26616ab..aead5d376e8c2f415925324f39dae6848dee46ce 100644 (file)
@@ -59,4 +59,21 @@ function update_record_status($id, $status, $table, $key) {
                
        db_query($sql, "Can't update record status");
 }
-?>
+//-----------------------------------------------------------------------------
+//
+//     Helper for sql subquery returning running totals from delta tables like stock_moves or bank_trans
+//
+//     $table - table name with optional WHERE clause
+//  $column - delta column
+//     $index  - comma delimited list of columns for total grouping and order
+//  Returns running totals with respective index column
+//
+function running_total_sql($table, $column, $index)
+{
+
+       return "SELECT daily.$index, daily.$column, (@total:=@total+daily.$column) total 
+               FROM
+                       (SELECT $index, sum($column) $column FROM $table GROUP BY $index ORDER BY $index) daily,
+                       (SELECT @total:=0) total_var";
+}
+
index a5e15fdbf4a524d1c7ded60d06f48ed4b6a75372..bf7570cd11d2ec0b19cda9c6abdc55322a1f70c9 100644 (file)
@@ -82,15 +82,32 @@ class items_cart
                return count($this->line_items);
        }
 
+       /*
+               Checks cart quantities on document_date.
+               Returns array of stock_ids which stock quantities would go negative on some day.
+       */
        function check_qoh($location, $date_, $reverse=false)
        {
+               $low_stock = array();
+
+               // collect quantities by stock_id
+               $qtys = array();
                foreach ($this->line_items as $line_no => $line_item)
                {
-                       $item_ret = $line_item->check_qoh($location, $date_, $reverse);
-                       if ($item_ret != null)
-                               return $line_no;
+                       $qty = $reverse ? -$line_item->quantity : $line_item->quantity;
+
+                       $qtys[$line_item->stock_id]['qty'] = $qty + @$qtys[$line_item->stock_id]['qty'];
+                       $qtys[$line_item->stock_id]['line'] = $line_no;
                }
-               return -1;
+
+               foreach($qtys as $stock_id => $sum)
+               {
+                       $fail = check_negative_stock($stock_id, $sum['qty'], $location, $date_);
+                       if ($fail)
+                               $low_stock[] = $stock_id;
+               }
+
+               return $low_stock;
        }
 
        // ----------- GL item functions
@@ -218,10 +235,14 @@ class line_item
                $this->price = 0;
        }
 
+       /*
+               This method is generally obsolete and subject to removal in FA 2.4 (preserved for now to support 2.3 extensions).
+               Use items_cart::check_qoh instead.
+       */
        function check_qoh($location, $date_, $reverse)
        {
                global $SysPrefs;
-               
+
        if (!$SysPrefs->allow_negative_stock())
        {
                        if (has_stock_holding($this->mb_flag))
@@ -233,11 +254,9 @@ class line_item
                                if ($quantity >= 0)
                                        return null;
 
-                               $qoh = get_qoh_on_date($this->stock_id, $location, $date_);
-                       if ($quantity + $qoh < 0)
-                       {
-                               return $this;
-                       }
+                               $fail = check_negative_stock($this->stock_id, $quantity, $location, $date_);
+                               if ($fail)
+                                       return $this;
                }
        }
 
index ea6d41ba20479b8336ab50c901cff150b5637869..dc14d93ef4a31edfbd3d3802c1edcda6cd29c1a6 100644 (file)
@@ -80,7 +80,7 @@ function handle_new_order()
 
 function can_process()
 {
-       global $Refs;
+       global $Refs, $SysPrefs;
 
        $adj = &$_SESSION['adj_items'];
 
@@ -114,16 +114,16 @@ function can_process()
                display_error(_("The entered date is not in fiscal year."));
                set_focus('AdjDate');
                return false;
-       } else {
-               $failed_item = $adj->check_qoh($_POST['StockLocation'], $_POST['AdjDate'], !$_POST['Increase']);
-               if ($failed_item >= 0) 
+       }
+       elseif (!$SysPrefs->allow_negative_stock())
+       {
+               $low_stock = $adj->check_qoh($_POST['StockLocation'], $_POST['AdjDate'], !$_POST['Increase']);
+
+               if ($low_stock)
                {
-                       $line = $adj->line_items[$failed_item];
-               display_error(_("The adjustment cannot be processed because an adjustment item would cause a negative inventory balance :") .
-                       " " . $line->stock_id . " - " .  $line->item_description);
-                       $_POST['Edit'.$failed_item] = 1; // enter edit mode
+               display_error(_("The adjustment cannot be processed because it would cause negative inventory balance for marked items as of document date or later."));
                        unset($_POST['Process']);
-               return false;
+                       return false;
                }
        }
        return true;
@@ -141,6 +141,7 @@ if (isset($_POST['Process']) && can_process()){
        unset($_SESSION['adj_items']);
 
        meta_forward($_SERVER['PHP_SELF'], "AddedID=$trans_no");
+
 } /*end of process credit note */
 
 //-----------------------------------------------------------------------------------------------
index f7af3dadcae60659f001def954399693757bd789..12571b8ac92e91454dc501738dcbe934f1ef341c 100644 (file)
@@ -17,7 +17,7 @@ include_once($path_to_root . "/includes/ui/items_cart.inc");
 function add_to_order(&$order, $new_item, $new_item_qty, $standard_cost)
 {
     if ($order->find_cart_item($new_item))
-         display_error(_("For Part :") . $new_item . " " . "This item is already on this order.  You can change the quantity ordered of the existing line if necessary.");
+         display_error(_("For Part :") . $new_item . " " . "This item is already on this document. You can change the quantity on the existing line if necessary.");
        else
          $order->add_to_cart (count($order->line_items), $new_item, $new_item_qty, $standard_cost);
 }
@@ -67,6 +67,7 @@ function display_adjustment_items($title, &$order)
        $total = 0;
        $k = 0;  //row colour counter
 
+       $low_stock = $order->check_qoh($_POST['StockLocation'], $_POST['AdjDate'], !$_POST['Increase']);
        $id = find_submit('Edit');
        foreach ($order->line_items as $line_no=>$stock_item)
        {
@@ -75,7 +76,10 @@ function display_adjustment_items($title, &$order)
 
                if ($id != $line_no)
                {
-               alt_table_row_color($k);
+                       if (in_array($stock_item->stock_id, $low_stock))
+                               start_row("class='stockmankobg'");      // notice low stock status
+                       else 
+                               alt_table_row_color($k);
 
                        view_stock_status_cell($stock_item->stock_id);
                        label_cell($stock_item->item_description);
@@ -102,6 +106,8 @@ function display_adjustment_items($title, &$order)
        label_row(_("Total"), number_format2($total,user_price_dec()), "align=right colspan=5", "align=right", 2);
 
     end_table();
+       if ($low_stock)
+               display_note(_("Marked items have insufficient quantities in stock as on day of adjustment."), 0, 1, "class='stockmankofg'");
        div_end();
 }
 
index 474fc9df31c692df9d9ee1fbf493365a47797190..026e0b6e07c7ff7b08f19521cb48488c40851c49 100644 (file)
@@ -17,7 +17,7 @@ include_once($path_to_root . "/includes/ui/items_cart.inc");
 function add_to_order(&$order, $new_item, $new_item_qty, $standard_cost)
 {
     if ($order->find_cart_item($new_item))
-         display_error(_("For Part :") . $new_item . " " . "This item is already on this order.  You can change the quantity ordered of the existing line if necessary.");
+         display_error(_("For Part :") . $new_item . " " . "This item is already on this document. You can change the quantity on the existing line if necessary.");
        else
          $order->add_to_cart (count($order->line_items), $new_item, $new_item_qty, $standard_cost);
 }
@@ -63,13 +63,17 @@ function display_transfer_items($title, &$order)
        $subtotal = 0;
        $k = 0;  //row colour counter
 
+       $low_stock = $order->check_qoh($_POST['FromStockLocation'], $_POST['AdjDate'], true);
        $id = find_submit('Edit');
        foreach ($order->line_items as $line_no=>$stock_item)
        {
 
                if ($id != $line_no)
                {
-               alt_table_row_color($k);
+                       if (in_array($stock_item->stock_id, $low_stock))
+                               start_row("class='stockmankobg'");      // notice low stock status
+                       else 
+                               alt_table_row_color($k);
 
                        view_stock_status_cell($stock_item->stock_id);
                label_cell($stock_item->item_description);
@@ -92,6 +96,8 @@ function display_transfer_items($title, &$order)
                transfer_edit_item_controls($order);
 
     end_table();
+       if ($low_stock)
+               display_note(_("Marked items have insufficient quantities in stock as on day of transfer."), 0, 1, "class='stockmankofg'");
        div_end();
 }
 
index 4ad32ea2451e739b29cfb856f0853a098923cb59..b04c7d87cd0db347d7f189407a957a23beb2908b 100644 (file)
@@ -115,17 +115,14 @@ if (isset($_POST['Process']))
                display_error(_("The locations to transfer from and to must be different."));
                set_focus('FromStockLocation');
                $input_error = 1;
-       } 
-       else 
+       }
+       elseif (!$SysPrefs->allow_negative_stock())
        {
-               $failed_item = $tr->check_qoh($_POST['FromStockLocation'], $_POST['AdjDate'], true);
-               if ($failed_item >= 0) 
+               $low_stock = $tr->check_qoh($_POST['FromStockLocation'], $_POST['AdjDate'], true);
+
+               if ($low_stock)
                {
-                       $line = $tr->line_items[$failed_item];
-               display_error(_("The quantity entered is greater than the available quantity for this item at the source location :") .
-                       " " . $line->stock_id . " - " .  $line->item_description);
-               echo "<br>";
-                       $_POST['Edit'.$failed_item] = 1; // enter edit mode
+               display_error(_("The transfer cannot be processed because it would cause negative inventory balance in source location for marked items as of document date or later."));
                        $input_error = 1;
                }
        }
index 7a2baacb143c8cc1d4936e73b4fc82c5ee15ab75..e70ce5711851e5c17a43370efd60c51f0a8b198e 100644 (file)
@@ -38,6 +38,8 @@ function display_issue_items($title, &$order)
 //     $total = 0;
        $k = 0;  //row colour counter
 
+       if (count($order->line_items))
+               $low_stock = $order->check_qoh($_POST['Location'], $_POST['date_'], !$_POST['IssueType']);
        $id = find_submit('Edit');
        foreach ($order->line_items as $line_no=>$stock_item)
        {
@@ -46,7 +48,10 @@ function display_issue_items($title, &$order)
 
                if ($id != $line_no)
                {
-               alt_table_row_color($k);
+                       if (in_array($stock_item->stock_id, $low_stock))
+                               start_row("class='stockmankobg'");      // notice low stock status
+                       else 
+                               alt_table_row_color($k);
 
                        view_stock_status_cell($stock_item->stock_id);
                        label_cell($stock_item->item_description);
@@ -73,6 +78,8 @@ function display_issue_items($title, &$order)
 //     label_row(_("Total"), number_format2($total,user_price_dec()), "colspan=5", "align=right");
 
     end_table();
+       if (@$low_stock)
+               display_note(_("Marked items have insufficient quantities in stock as on day of issue."), 0, 1, "class='stockmankofg'");
        div_end();
 }
 
index acf67f73e197ef7e0621795772b40b17426a1893..fb2ae94ca33d6a21d706e0b4abb833c933448ba3 100644 (file)
@@ -121,10 +121,7 @@ function can_process()
        // if unassembling we need to check the qoh
        if (($_POST['ProductionType'] == 0) && !$SysPrefs->allow_negative_stock())
        {
-               $wo_details = get_work_order($_POST['selected_id']);
-
-               $qoh = get_qoh_on_date($wo_details["stock_id"], $wo_details["loc_code"], $_POST['date_']);
-               if (-input_num('quantity') + $qoh < 0)
+               if (check_negative_stock($wo_details["stock_id"], -input_num('quantity'), $wo_details["loc_code"], $_POST['date_']))
                {
                        display_error(_("The unassembling cannot be processed because there is insufficient stock."));
                        set_focus('quantity');
@@ -141,13 +138,13 @@ function can_process()
                {
                        if ($row['mb_flag'] == 'D') // service, non stock
                                continue;
-                       $qoh = get_qoh_on_date($row["stock_id"], $row["loc_code"], $_POST['date_']);
-                       if ($qoh - $row['units_req'] * input_num('quantity') < 0)
+
+                       if (check_negative_stock($row["stock_id"], -$row['units_req'] * input_num('quantity'), $row["loc_code"], $_POST['date_']))
                        {
                        display_error( _("The production cannot be processed because a required item would cause a negative inventory balance :") .
                                " " . $row['stock_id'] . " - " .  $row['description']);
-                       $err = true;    
-                       }       
+                       $err = true;
+                       }
                }
                if ($err)
                {
index f5e90d10c55ed299bd5920610911a3912880b072..049ab914527e0609b0de9e24ad6e0f3276dfc09e 100644 (file)
@@ -203,8 +203,7 @@ function can_process()
 
                                $quantity = $bom_item["quantity"] * input_num('quantity');
 
-                        $qoh = get_qoh_on_date($bom_item["component"], $bom_item["loc_code"], $_POST['date_']);
-                               if (-$quantity + $qoh < 0)
+                        if (check_negative_stock($bom_item["component"], -$quantity, $bom_item["loc_code"], $_POST['date_']))
                                {
                                        display_error(_("The work order cannot be processed because there is an insufficient quantity for component:") .
                                                " " . $bom_item["component"] . " - " .  $bom_item["description"] . ".  " . _("Location:") . " " . $bom_item["location_name"]);
@@ -217,8 +216,7 @@ function can_process()
                elseif ($_POST['type'] == WO_UNASSEMBLY)
                {
                        // if unassembling, check item to unassemble
-                               $qoh = get_qoh_on_date($_POST['stock_id'], $_POST['StockLocation'], $_POST['date_']);
-                       if (-input_num('quantity') + $qoh < 0)
+                if (check_negative_stock($_POST['stock_id'], -input_num('quantity'), $_POST['StockLocation'], $_POST['date_']))
                        {
                                display_error(_("The selected item cannot be unassembled because there is insufficient stock."));
                                        return false;
index 071bf3c5856f0b1c865aa3685bec1ff3ca66bcf4..cfd4d19761fb49d3e7d841b78c1155906c5bcb64 100644 (file)
@@ -101,8 +101,7 @@ function can_process()
        $failed_item = $_SESSION['issue_items']->check_qoh($_POST['Location'], $_POST['date_'], !$_POST['IssueType']);
        if ($failed_item != -1) 
        {
-       display_error( _("The issue cannot be processed because an entered item would cause a negative inventory balance :") .
-               " " . $failed_item->stock_id . " - " .  $failed_item->item_description);
+               display_error(_("The issue cannot be processed because it would cause negative inventory balance for marked items as of document date or later."));
                return false;
        }
 
index 57e8696eda925d5ef974100877afc3213f0a7632..b4f31966d9aa7f8f7e82368b5625a6960c0a5991 100644 (file)
@@ -204,8 +204,7 @@ function check_data()
                foreach ($_SESSION['supp_trans']->grn_items as $n => $item) {
                        if (is_inventory_item($item->item_code))
                        {
-                               $qoh = get_qoh_on_date($item->item_code, null, $_SESSION['supp_trans']->tran_date);
-                               if ($item->this_quantity_inv > $qoh)
+                               if (check_negative_stock($item->item_code, -$item->this_quantity_inv, null, $_SESSION['supp_trans']->tran_date))
                                {
                                        $stock = get_item($item->item_code);
                                        display_error(_("The return cannot be processed because there is an insufficient quantity for item:") .
index 114f3d4a9ccf0331f207d1c8561ae9bcb0cf06d9..36dc0b49b9acd4100ff4b8c66bca626dae547ab1 100644 (file)
@@ -141,7 +141,7 @@ if (isset($_GET['OrderNumber']) && $_GET['OrderNumber'] > 0) {
 
 function check_data()
 {
-       global $Refs;
+       global $Refs, $SysPrefs;
 
        if (!isset($_POST['DispatchDate']) || !is_date($_POST['DispatchDate'])) {
                display_error(_("The entered date of delivery is invalid."));
@@ -187,6 +187,14 @@ function check_data()
                return false;
        }
 
+       copy_to_cart();
+
+       if (!$SysPrefs->allow_negative_stock() && ($low_stock = $_SESSION['Items']->check_qoh()))
+       {
+               display_error(_("This document cannot be processed because there is insufficient quantity for: ").implode(',', $low_stock));
+               return false;
+       }
+
        return true;
 }
 //------------------------------------------------------------------------------
@@ -236,7 +244,7 @@ function check_quantities()
                        $min = 0;
                        $max = $itm->quantity - $itm->qty_done;
                }
-               
+
                        if (check_num('Line'.$line, $min, $max)) {
                                $_SESSION['Items']->line_items[$line]->qty_dispatched =
                                  input_num('Line'.$line);
@@ -261,40 +269,8 @@ function check_quantities()
 }
 //------------------------------------------------------------------------------
 
-function check_qoh()
+if (isset($_POST['process_delivery']) && check_data())
 {
-    global $SysPrefs;
-    $dn = &$_SESSION['Items'];
-    $newdelivery = ($dn->trans_no==0);
-    if (!$SysPrefs->allow_negative_stock()) {
-        foreach ($_SESSION['Items']->line_items as $itm) {
-
-            if ($itm->qty_dispatched && has_stock_holding($itm->mb_flag)) {
-                $qoh_by_date = get_qoh_on_date($itm->stock_id, $_POST['Location'], $_POST['DispatchDate']);
-                $qoh_abs = get_qoh_on_date($itm->stock_id, $_POST['Location'], null);
-                //If editing current delivery delivered qty should be added 
-                if (!$newdelivery)
-                {
-                    $delivered = get_already_delivered($itm->stock_id, $_POST['Location'], key($dn->trans_no));
-                    
-                    $qoh_abs = $qoh_abs - $delivered;
-                    $qoh_by_date = $qoh_by_date - $delivered;
-                }
-                $qoh = ($qoh_by_date < $qoh_abs ? $qoh_by_date : $qoh_abs); 
-                if ($itm->qty_dispatched > $qoh) {
-                    display_error(_("The delivery cannot be processed because there is an insufficient quantity for item:") .
-                        " " . $itm->stock_id . " - " . $itm->item_description);
-                    return false;
-                }
-            }
-        }
-    }
-    return true;
-}
-
-//------------------------------------------------------------------------------
-
-if (isset($_POST['process_delivery']) && check_data() && check_qoh()) {
        $dn = &$_SESSION['Items'];
 
        if ($_POST['bo_policy']) {
@@ -304,9 +280,11 @@ if (isset($_POST['process_delivery']) && check_data() && check_qoh()) {
        }
        $newdelivery = ($dn->trans_no == 0);
 
-       copy_to_cart();
-       if ($newdelivery) new_doc_date($dn->document_date);
+       if ($newdelivery)
+               new_doc_date($dn->document_date);
+
        $delivery_no = $dn->write($bo_policy);
+
        if ($delivery_no == -1)
        {
                display_error(_("The entered reference is already in use."));
@@ -320,10 +298,10 @@ if (isset($_POST['process_delivery']) && check_data() && check_qoh()) {
                } else {
                        meta_forward($_SERVER['PHP_SELF'], "UpdatedID=$delivery_no");
                }
-       }       
+       }
 }
 
-if (isset($_POST['Update']) || isset($_POST['_Location_update']) || isset($_POST['qty'])) {
+if (isset($_POST['Update']) || isset($_POST['_Location_update']) || isset($_POST['qty']) || isset($_POST['process_delivery'])) {
        $Ajax->activate('Items');
 }
 //------------------------------------------------------------------------------
@@ -451,16 +429,22 @@ foreach ($_SESSION['Items']->line_items as $line=>$ln_itm) {
                // quantity input box. This allows for example a hook to modify the default quantity to what's dispatchable
                // (if there is not enough in hand), check at other location or other order people etc ...
                // This hook also returns a 'reason' (css classes) which can be used to theme the row.
+               //
+               // FIXME: hook_get_dispatchable definition does not allow qoh checks on transaction level
+               // (but anyway dispatch is checked again later before transaction is saved)
+
+               $qty = $ln_itm->qty_dispatched;
+               if ($check = check_negative_stock($ln_itm->stock_id, -$ln_itm->qty_dispatched, $_POST['Location'], $_POST['DispatchDate']))
+                       $qty = $check['qty'];
+
+               $q_class =  hook_get_dispatchable_quantity($ln_itm, $_POST['Location'], $_POST['DispatchDate'], $qty);
 
-               $qoh = get_qoh_on_date($ln_itm->stock_id, $_POST['Location'], $_POST['DispatchDate']);
-               $q_class =  hook_get_dispatchable_quantity($ln_itm, $_POST['Location'], $_POST['DispatchDate'], $qoh);
                // Skip line if needed
                if($q_class === 'skip')  continue;
                if(is_array($q_class)) {
                  list($ln_itm->qty_dispatched, $row_classes) = $q_class;
                        $has_marked = true;
                }
-
        }
 
        alt_table_row_color($k, $row_classes);
@@ -520,7 +504,7 @@ label_row(_("Amount Total"), $display_total, "colspan=$colspan align=right","ali
 end_table(1);
 
 if ($has_marked) {
-       display_note(_("Marked items have insufficient quantities in stock as on day of delivery."), 0, 1, "class='red'");
+       display_note(_("Marked items have insufficient quantities in stock as on day of delivery."), 0, 1, "class='stockmankofg'");
 }
 start_table(TABLESTYLE2);
 
index 64a237f320aa824db88f3377e653e6615cbc7132..e39ebf997b10bde2368bb00cbccda9f5b14461e3 100644 (file)
@@ -544,6 +544,40 @@ class cart
 
                return $total;
        }
+
+       /*
+               Checks cart quantities on document_date.
+               Returns array of stock_ids which stock quantities would go negative on some day.
+       */
+       function check_qoh($date=null, $location=null)
+       {
+               $low_stock = array();
+               // check only for customer delivery and direct sales invoice 
+               if (!($this->trans_type == ST_CUSTDELIVERY || ($this->trans_type == ST_SALESINVOICE && $this->trans_no==0)))
+                       return $low_stock;
+
+               // collect quantities by stock_id
+               $qtys = array();
+               foreach ($this->line_items as $line_no => $line_item)
+               {
+                       if (has_stock_holding($line_item->mb_flag))
+                       {
+                               if (!$this->trans_no) // new delivery
+                                       $qtys[$line_item->stock_id]['qty'] = $line_item->quantity + @$qtys[$line_item->stock_id]['qty'];
+                               else    // DN modification: check change in quantity
+                                       $qtys[$line_item->stock_id]['qty'] = ($line_item->qty_dispatched-$line_item->quantity) + @$qtys[$line_item->stock_id]['qty'];
+                               $qtys[$line_item->stock_id]['line'] = $line_no;
+                       }
+               }
+               foreach($qtys as $stock_id => $sum)
+               {
+                       if (check_negative_stock($stock_id, -$sum['qty'], $location ? $location : $this->Location, $date ? $date : $this->document_date))
+                               $low_stock[] = $stock_id;
+               }
+
+               return $low_stock;
+       }
+
 } /* end of class defintion */
 
 class line_details
index 2af450093abb0b65674e65a839d28c378db532cd..a07ceba00c508c1a885d7a35bd61c857b72de47e 100644 (file)
@@ -127,7 +127,7 @@ function get_customer_details_to_order(&$order, $customer_id, $branch_id)
                $order->set_location($order->pos["pos_location"], $order->pos["location_name"]);
        } else
                $order->set_location($myrow["default_location"], $myrow["location_name"]);
-               
+
        return $ret_error;
 }
 
@@ -158,8 +158,8 @@ function display_order_summary($title, &$order, $editable_items=false)
        $k = 0;  //row colour counter
 
        $id = find_submit('Edit');
-       $has_marked = false;
 
+       $low_stock = $order->check_qoh($_POST['OrderDate'], $_POST['Location']);
        foreach ($order->get_items() as $line_no=>$stock_item)
        {
 
@@ -169,23 +169,10 @@ function display_order_summary($title, &$order, $editable_items=false)
                $qoh_msg = '';
                if (!$editable_items || $id != $line_no)
                {
-                       if (!$SysPrefs->allow_negative_stock() && is_inventory_item($stock_item->stock_id) && 
-                               $order->trans_type != ST_SALESORDER && $order->trans_type!=ST_SALESQUOTE) {
-                               $qoh = get_qoh_on_date($stock_item->stock_id, 
-                                       $_POST['Location'], $_POST['delivery_date']);
-                               if (($stock_item->qty_dispatched -$stock_item->qty_done) > $qoh)
-                               {
-                                       // oops, we don't have enough of one of the component items
-                                       start_row("class='stockmankobg'");
-                                       $qoh_msg .= $stock_item->stock_id . " - " . $stock_item->item_description . ": " .
-                                       _("Quantity On Hand") . " = " 
-                                       . number_format2($qoh, get_qty_dec($stock_item->stock_id)) . '<br>';
-                                       $has_marked = true;
-                                } else 
-                                       alt_table_row_color($k);
-                       } else {
+                       if (in_array($stock_item->stock_id, $low_stock))
+                               start_row("class='stockmankobg'");      // notice low stock status
+                       else 
                                alt_table_row_color($k);
-                       }
 
                        view_stock_status_cell($stock_item->stock_id);
 
@@ -244,12 +231,9 @@ function display_order_summary($title, &$order, $editable_items=false)
        end_row();
 
        end_table();
-       if ($has_marked) {
+       if ($low_stock)
                display_note(_("Marked items have insufficient quantities in stock as on day of delivery."), 0, 1, "class='stockmankofg'");
-               if ($order->trans_type!=30 && !$SysPrefs->allow_negative_stock())
-                       display_error(_("The delivery cannot be processed because there is an insufficient quantity for item:")
-                               . '<br>'. $qoh_msg);
-       }
+
     div_end();
 }
 
index 90e768398ddc8d768d7bfb8566978b9589fc79df..3873d4a502b0c76c655151757f812c150665305b 100644 (file)
@@ -333,7 +333,10 @@ function line_start_focus() {
 
 //--------------------------------------------------------------------------------
 function can_process() {
-       global $Refs;
+
+       global $Refs, $SysPrefs;
+
+       copy_to_cart();
 
        if (!get_post('customer_id')) 
        {
@@ -364,14 +367,20 @@ function can_process() {
                set_focus('AddItem');
                return false;
        }
-       if ($_SESSION['Items']->payment_terms['cash_sale'] == 0) {
-       if (strlen($_POST['deliver_to']) <= 1) {
-               display_error(_("You must enter the person or company to whom delivery should be made to."));
-               set_focus('deliver_to');
+
+       if (!$SysPrefs->allow_negative_stock() && ($low_stock = $_SESSION['Items']->check_qoh()))
+       {
+               display_error(_("This document cannot be processed because there is insufficient quantity for items marked."));
                return false;
        }
 
+       if ($_SESSION['Items']->payment_terms['cash_sale'] == 0) {
 
+               if (strlen($_POST['deliver_to']) <= 1) {
+                       display_error(_("You must enter the person or company to whom delivery should be made to."));
+                       set_focus('deliver_to');
+                       return false;
+               }
                if ($_SESSION['Items']->trans_type != ST_SALESQUOTE && strlen($_POST['delivery_address']) <= 1) {
                        display_error( _("You should enter the street address in the box provided. Orders cannot be accepted without a valid street address."));
                        set_focus('delivery_address');
@@ -394,7 +403,6 @@ function can_process() {
                        set_focus('delivery_date');
                        return false;
                }
-               //if (date1_greater_date2($_SESSION['Items']->document_date, $_POST['delivery_date'])) {
                if (date1_greater_date2($_POST['OrderDate'], $_POST['delivery_date'])) {
                        if ($_SESSION['Items']->trans_type==ST_SALESQUOTE)
                                display_error(_("The requested valid date is before the date of the quotation."));
@@ -435,7 +443,7 @@ if (isset($_POST['update'])) {
 }
 
 if (isset($_POST['ProcessOrder']) && can_process()) {
-       copy_to_cart();
+
        $modified = ($_SESSION['Items']->trans_no != 0);
        $so_type = $_SESSION['Items']->so_type;
 
@@ -505,23 +513,8 @@ function check_item_data()
                set_focus('qty');
                display_error(_("You attempting to make the quantity ordered a quantity less than has already been delivered. The quantity delivered cannot be modified retrospectively."));
                return false;
-       } // Joe Hunt added 2008-09-22 -------------------------
-       elseif ($is_inventory_item && $_SESSION['Items']->trans_type!=ST_SALESORDER && $_SESSION['Items']->trans_type!=ST_SALESQUOTE 
-               && !$SysPrefs->allow_negative_stock())
-       {
-               $qoh = get_qoh_on_date($_POST['stock_id'], $_POST['Location'], $_POST['OrderDate']);
-               $qoh_today = get_qoh_on_date($_POST['stock_id'], $_POST['Location'], null); // Of course we must also test for stock availability by todays date.
-               $qty = input_num('qty');
-               if ($qty > $qoh || $qty > $qoh_today)
-               {
-                       $stock = get_item($_POST['stock_id']);
-                       display_error(_("The delivery cannot be processed because there is an insufficient quantity for item:") .
-                               " " . $stock['stock_id'] . " - " . $stock['description'] . " - " .
-                               _("Quantity On Hand") . " = " . number_format2($qoh, get_qty_dec($_POST['stock_id'])));
-                       return false;
-               }
-               return true;
        }
+
        $cost_home = get_standard_cost(get_post('stock_id')); // Added 2011-03-27 Joe Hunt
        $cost = $cost_home / get_exchange_rate_from_home_currency($_SESSION['Items']->customer_currency, $_SESSION['Items']->document_date);
        if (input_num('price') < $cost)