CORE-POS/IS4C

View on GitHub
pos/is4c-nf/parser/parse/VoidCmd.php

Summary

Maintainability
F
3 days
Test Coverage
D
67%
<?php
/*******************************************************************************

    Copyright 2007 Whole Foods Co-op

    This file is part of IT CORE.

    IT CORE is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    IT CORE is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    in the file license.txt along with IT CORE; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

*********************************************************************************/

namespace COREPOS\pos\parser\parse;
use COREPOS\pos\lib\Database;
use COREPOS\pos\lib\DisplayLib;
use COREPOS\pos\lib\MiscLib;
use COREPOS\pos\lib\PrehLib;
use COREPOS\pos\lib\TransRecord;
use COREPOS\pos\lib\adminlogin\AdminLoginInterface;
use COREPOS\pos\parser\Parser;
use \CoreLocal;
use \Exception;

class VoidCmd extends Parser implements AdminLoginInterface
{
    private $scaleprice = 0;

    public function check($str)
    {
        if (substr($str,0,2) == "VD" && strlen($str) <= 15) {
            return true;
        }

        return false;
    }

    public function parse($str)
    {
        $ret = $this->default_json();
    
        try {
            $this->checkVoidLimit(0);
            if (strlen($str) > 2) {
                $ret = $this->voidupc(substr($str,2), $ret);
            } elseif ($this->session->get("currentid") == 0) {
                $ret['output'] = DisplayLib::boxMsg(
                    _("No Item on Order"),
                    '',
                    false,
                    DisplayLib::standardClearButton()
                );
            } else {
                $transID = $this->session->get("currentid");

                $status = $this->checkstatus($transID);
                $this->scaleprice = $status['scaleprice'];

                $ret = $this->branchByVoided($status['voided'], $transID, $status['status'], $ret);
            }

            if (empty($ret['output']) && empty($ret['main_frame'])) {
                $ret['output'] = DisplayLib::lastpage();
                $ret['redraw_footer'] = true;
                $ret['udpmsg'] = 'goodBeep';
            } elseif (empty($ret['main_frame'])) {
                $ret['udpmsg'] = 'errorBeep';
            }
        } catch (Exception $ex) {
            $ret['main_frame'] = $ex->getMessage();
        }

        return $ret;
    }

    private function branchByVoided($voided, $transID, $status, $ret)
    {
        /**
          Voided values:
            2 => "you saved" line
            3 => subtotal line
            4 => discount notice
            5 => % Discount line
            6 => tare weight, case disc notice,
            8 => FS change, regular change
            10 => tax exempt
        */
        if ($voided == 2) {
            // void preceeding item
            $ret = $this->voiditem($transID - 1, $ret);
        } elseif ($voided == 3 || $voided == 6 || $voided == 8) {
            $ret['output'] = DisplayLib::boxMsg(
                _("Cannot void this entry"),
                '',
                false,
                DisplayLib::standardClearButton()
            );
        } elseif ($voided == 4 || $voided == 5) {
            PrehLib::percentDiscount(0);
        } elseif ($voided == 10) {
            TransRecord::reverseTaxExempt();
        } elseif ($status == "V") {
            $ret['output'] = DisplayLib::boxMsg(
                _("Item already voided"),
                '',
                false,
                DisplayLib::standardClearButton()
            );
        } else {
            $ret = $this->voiditem($transID, $ret);
        }

        return $ret;
    }

    /**
      Lookup item and decide whether to void
      by simply reversing the record or by
      applying product UPC
      @param $itemNum localtemptrans.trans_id value to void
      @param $json parser return value structure
    */
    public function voiditem($itemNum, $json)
    {
        if ($itemNum) {
            $row = $this->getLine($itemNum);

            if (!$row) {
                $json['output'] = DisplayLib::boxMsg(
                    _("Item not found"),
                    '',
                    false,
                    DisplayLib::standardClearButton()
                );
            } elseif ($row['voided'] == 1 && $this->voidableNonUpc($row)) {
                $json['output'] = DisplayLib::boxMsg(
                    _("Item already voided"),
                    '',
                    false,
                    DisplayLib::standardClearButton()
                );
            } elseif ($this->voidableNonUpc($row)) {
                $json = $this->voidid($itemNum, $json);
            } else {
                $json = $this->voidupc($row["ItemQtty"] . "*" . $row["upc"], $json, $itemNum);
            }
        } else {
            $json['output'] = DisplayLib::boxMsg(
                _("Item not found"),
                '',
                false,
                DisplayLib::standardClearButton()
            );
        }

        return $json;
    }

    private function voidableNonUpc($row)
    {
        return (
            !$row["upc"] 
            || strlen($row["upc"]) < 1 
            || $row['trans_type'] === 'D'
            || $row['charflag'] === 'SO'
            || $row['charflag'] === 'TF'
        );
    }

    private function getLine($itemNum)
    {
        $query = "select upc,VolSpecial,quantity,trans_subtype,unitPrice,
            discount,memDiscount,discountable,scale,numflag,charflag,
            foodstamp,discounttype,total,cost,description,trans_type,
            department,regPrice,tax,volDiscType,volume,mixMatch,matched,
            trans_status,ItemQtty,voided
                   from localtemptrans where trans_id = ".$itemNum;
        $dbc = Database::tDataConnect();
        $result = $dbc->query($query);
        return $dbc->fetchRow($result);
    }

    /**
      Void record by trans_id
      @param $itemNum [int] trans_id
      @param $json parser return value structure

      This marks the specified record as voided
      and adds an offsetting record also marked voided
      Neither record can be subsequently modified via
      voids.
    */
    public function voidid($itemNum, $json)
    {
        $row = $this->getLine($itemNum);

        $upc = $row["upc"];
        $volSpecial = $row["VolSpecial"];
        $quantity = -1 * $row["quantity"];
        $total = -1 * $row["total"];
        // 11Jun14 Andy => don't know why FS is different. legacy?
        if ($row["trans_subtype"] == "FS") {
            $total = -1 * $row["unitPrice"];
        } elseif ($row['trans_status'] == 'R' && $row['trans_type'] == 'D') {
            // set refund flag and let that logic reverse
            // the total and quantity
            $this->session->set('refund', 1);
            $total = $row['total'];
            $quantity = $row['quantity'];
        }
        $discount = -1 * $row["discount"];
        $memDiscount = -1 * $row["memDiscount"];
        $discountable = $row["discountable"];
        $unitPrice = $row["unitPrice"];
        $scale = MiscLib::nullwrap($row["scale"]);
        $cost = -1 * $row['cost'];
        $numflag = $row["numflag"];
        $charflag = $row["charflag"];
        $mixmatch = $row['mixMatch'];
        $matched = $row['matched'];

        $foodstamp = 0;
        if ($row["foodstamp"] != 0) {
            $foodstamp = 1;
        }

        $discounttype = MiscLib::nullwrap($row["discounttype"]);

        try {
            if ($row['trans_type'] == 'D') {
                $this->checkVoidLimit($row['total']);
            }
            $this->checkTendered($total);
            $dbc = Database::tDataConnect();
            $update = "update localtemptrans set voided = 1 where trans_id = ".$itemNum;
            $dbc->query($update);

            TransRecord::addRecord(array(
                'upc' => $upc, 
                'description' => $row["description"], 
                'trans_type' => $row["trans_type"], 
                'trans_subtype' => $row["trans_subtype"], 
                'trans_status' => "V", 
                'department' => $row["department"], 
                'quantity' => $quantity, 
                'unitPrice' => $unitPrice, 
                'total' => $total, 
                'regPrice' => $row["regPrice"], 
                'scale' => $scale, 
                'tax' => $row["tax"], 
                'foodstamp' => $foodstamp, 
                'discount' => $discount, 
                'memDiscount' => $memDiscount, 
                'discountable' => $discountable, 
                'discounttype' => $discounttype, 
                'ItemQtty' => $quantity, 
                'volDiscType' => $row["volDiscType"], 
                'volume' => $row["volume"], 
                'VolSpecial' => $volSpecial, 
                'mixMatch' => $mixmatch, 
                'matched' => $matched, 
                'voided' => 1, 
                'cost' => $cost, 
                'numflag' => $numflag, 
                'charflag' => $charflag
            ));

            if ($row["trans_type"] != "T") {
                $this->session->set("ttlflag",0);
            } else {
                PrehLib::ttl();
            }

        } catch (Exception $ex) {
            if ($ex->getCode() == 0) {
                $json['output'] = $ex->getMessage();
            } elseif ($ex->getCode() == 1) {
                $json['main_frame'] = $ex->getMessage();
            }
        }

        return $json;
    }

    private function upcQuantity($upc)
    {
        /**
          If UPC contains an asterisk, extract quantity
          and validate input. Otherwise use quantity 1.
        */
        if (strstr($upc, '*')) {
            list($quantity, $upc) = explode('*', $upc, 2);
            if ($quantity === '' || $upc === '' || !is_numeric($quantity) || !is_numeric($upc)) {
                throw new Exception(DisplayLib::inputUnknown());
            }
            $weight = 0;
        } else {
            $quantity = 1;
            $weight = $this->session->get("weight");
        }
        
        return array($upc, $quantity, $weight);
    }

    private function scaleUPC($upc)
    {
        $scaleprice = 0;
        $deliflag = false;
        if (is_numeric($upc)) {
            $upc = substr("0000000000000" . $upc, -13);
            if (substr($upc, 0, 3) == "002" && substr($upc, -5) != "00000") {
                $scaleprice = substr($upc, 10, 4)/100;
                $upc = substr($upc, 0, 8) . "0000";
                $deliflag = true;
            } elseif (substr($upc, 0, 3) == "002" && substr($upc, -5) == "00000") {
                $scaleprice = $this->scaleprice;
                $deliflag = true;
            }
        }

        return array($upc, $scaleprice, $deliflag);
    }

    private function findUPC($upc, $deliflag, $scaleprice)
    {
        $dbc = Database::tDataConnect();

        $query = "SELECT SUM(ItemQtty) AS voidable, 
                    SUM(quantity) AS vquantity,
                    MAX(scale) AS scale,
                    MAX(volDiscType) AS volDiscType 
                  FROM localtemptrans 
                  WHERE upc = '" . $upc . "'";
        if ($deliflag) {
            $query .= ' AND unitPrice = ' . $scaleprice;
        }
        $query .= ' GROUP BY upc';

        $result = $dbc->query($query);
        $numRows = $dbc->numRows($result);
        if ($numRows == 0 ) {
            throw new Exception(DisplayLib::boxMsg(
                _("Item not found: ") . $upc,
                '',
                false,
                DisplayLib::standardClearButton()
            ));
        }

        return $dbc->fetchRow($result);
    }

    private function checkUpcQuantities($voidable, $quantity, $scale, $row)
    {
        if ($voidable == 0 && $quantity == 1) {
            throw new Exception(DisplayLib::boxMsg(
                _("Item already voided"),
                '',
                false,
                DisplayLib::standardClearButton()
            ));
        } elseif ($voidable == 0 && $quantity > 1) {
            throw new Exception(DisplayLib::boxMsg(
                _("Items already voided"),
                '',
                false,
                DisplayLib::standardClearButton()
            ));
        } elseif ($scale == 1 && $quantity < 0) {
            throw new Exception(DisplayLib::boxMsg(
                _("tare weight cannot be greater than item weight"),
                '',
                false,
                DisplayLib::standardClearButton()
            ));
        } elseif ($voidable < $quantity && $scale == 1) {
            $message = _("Void request exceeds")."<br />"._("weight of item rung in")."<p><b>".
                sprintf(_("You can void up to %.2f lb"),$row['voidable'])."</b>";
            throw new Exception(DisplayLib::boxMsg($message, '', false, DisplayLib::standardClearButton()));
        } elseif ($voidable < $quantity) {
            $message = _("Void request exceeds")."<br />"._("number of items rung in")."<p><b>".
                sprintf(_("You can void up to %d"),$row['voidable'])."</b>";
            throw new Exception(DisplayLib::boxMsg($message, '', false, DisplayLib::standardClearButton()));
        }

        return true;
    }

    private function findUpcLine($upc, $scaleprice, $deliflag, $itemNum)
    {
        $dbc = Database::tDataConnect();
        //----------------------Void Item------------------
        $queryUPC = "SELECT 
                        ItemQtty,
                        foodstamp,
                        discounttype,
                        mixMatch,
                        cost,
                        numflag,
                        charflag,
                        unitPrice,
                        total,
                        discounttype,
                        regPrice,
                        discount,
                        memDiscount,
                        discountable,
                        description,
                        trans_type,
                        trans_subtype,
                        department,
                        tax,
                        VolSpecial,
                        matched,
                        scale,
                        trans_id
                      FROM localtemptrans 
                      WHERE upc = '" . $upc . "'"; 
        if ($deliflag) {
            $queryUPC .= ' AND unitPrice = ' . $scaleprice;
        }
        if ($itemNum != -1) {
            $queryUPC .= ' AND trans_id = ' . $itemNum . ' AND voided=0 ';
        } else {
            $queryUPC .= ' AND voided=0 ORDER BY total';
        }

        $result = $dbc->query($queryUPC);
        if ($dbc->numRows($result) > 0) {
            return $dbc->fetchRow($result);
        }

        return $this->findUpcLine($upc, $scaleprice, $deliflag, -1);
    }

    private function adjustUnitPrice($upc, $row)
    {
        $unitPrice = $row['unitPrice'];
        /**
          11Jun14 Andy
          Convert unitPrice to/from sale price based on
          member status. I'm not sure this is actually
          necessary.
        */
        if (($this->session->get("isMember") != 1 && $row["discounttype"] == 2) || 
            ($this->session->get("isStaff") == 0 && $row["discounttype"] == 4)) {
            $unitPrice = $row["regPrice"];
        } elseif ((($this->session->get("isMember") == 1 && $row["discounttype"] == 2) || 
            ($this->session->get("isStaff") != 0 && $row["discounttype"] == 4)) && 
            ($row["unitPrice"] == $row["regPrice"])) {
            $dbcp = Database::pDataConnect();
            $queryp = "select special_price from products where upc = '".$upc."'";
            $resultp = $dbcp->query($queryp);
            $rowp = $dbcp->fetchRow($resultp);
            
            $unitPrice = $rowp["special_price"];
        }

        return $unitPrice;
    }

    private function checkVoidLimit($total)
    {
        /**
          Check if the voiding item will exceed the limit. If so,
          prompt for admin password. 
        */
        if (is_numeric($this->session->get('VoidLimit')) && $this->session->get('VoidLimit') > 0) {
            $currentTotal = $this->session->get('voidTotal');
            if ($currentTotal + (-1*$total) > $this->session->get('VoidLimit') && $this->session->get('voidOverride') != 1) {
                $this->session->set('strRemembered', $this->session->get('strEntered'));
                $this->session->set('voidOverride', 0);
                throw new Exception(MiscLib::baseURL().'gui-modules/adminlogin.php?class=COREPOS-pos-parser-parse-VoidCmd', 1);
            }
        }
    }

    private function checkTendered($total)
    {
        if ($this->session->get("tenderTotal") < 0 && (-1 * $total) > $this->session->get("runningTotal") - $this->session->get("taxTotal")) {
            $dbc = Database::tDataConnect();
            $cash = $dbc->query("SELECT total FROM localtemptrans WHERE trans_subtype='CA' AND total <> 0");
            if ($dbc->numRows($cash) > 0) {
                throw new Exception(DisplayLib::boxMsg(
                    _("Item already paid for"),
                    '',
                    false,
                    DisplayLib::standardClearButton()
                ));
            }
        }
    }

    /**
      Void the given UPC
      @param $upc [string] upc to void. Optionally including quantity and asterisk
      @param $itemNum [int] trans_id of record to void. Optional.
      @param $json parser return value structure
    */
    public function voidupc($upc, $json, $itemNum=-1)
    {
        $deliflag = false;
        $quantity = 0;

        try {
            list($upc, $quantity, $weight) = $this->upcQuantity($upc);
            list($upc, $scaleprice, $deliflag) = $this->scaleUPC($upc);
            $row = $this->findUPC($upc, $deliflag, $scaleprice);

            if (($row["scale"] == 1) && $weight > 0) {
                $quantity = $weight - $this->session->get("tare");
                $this->session->set("tare", 0);
            }
            $volDiscType = $row["volDiscType"];
            $voidable = MiscLib::nullwrap($row["voidable"]);
            $volSpecial = 0;
            $volume = 0;
            $scale = MiscLib::nullwrap($row["scale"]);
            $this->checkUpcQuantities($voidable, $quantity, $scale, $row);

            $row = $this->findUpcLine($upc, $scaleprice, $deliflag, $itemNum);
            // if the selected line was already voided, findUpcLine() might locate
            // a different, equivalent line to proceed with
            $itemNum = $row['trans_id'];
            $foodstamp = MiscLib::nullwrap($row["foodstamp"]);
            $discounttype = MiscLib::nullwrap($row["discounttype"]);
            $mixMatch = MiscLib::nullwrap($row["mixMatch"]);
            $matched = -1 * $row['matched'];
            $cost = -1* $row['cost'];
            $numflag = $row['numflag'];
            $charflag = $row['charflag'];
            $unitPrice = $this->adjustUnitPrice($upc, $row);
            $discount = -1 * $row["discount"];
            $memDiscount = -1 * $row["memDiscount"];
            $discountable = $row["discountable"];
            $quantity = -1 * $quantity;
            $total = $quantity * $unitPrice;
            if ($row['unitPrice'] == 0) {
                $total = $quantity * $row['total'];
            } elseif ($row['total'] != $total && $row['scale'] == 1) {
                /**
                  If the total does not match quantity times unit price,
                  the cashier probably manually specified a quantity
                  i.e., VD{qty}*{upc}. This is probably OK for non-weight
                  items. Each record should be the same and voiding multiple
                  in one line will usually be fine.
                */
                $total = -1*$row['total'];
            }
            $this->checkVoidLimit($total);
            $this->checkTendered($total);

            if ($quantity != 0) {
                $dbc = Database::tDataConnect();

                $update = "update localtemptrans set voided = 1 where trans_id = ".$itemNum;
                $dbc->query($update);

                TransRecord::addRecord(array(
                    'upc' => $upc, 
                    'description' => $row["description"], 
                    'trans_type' => $row["trans_type"], 
                    'trans_subtype' => $row["trans_subtype"], 
                    'trans_status' => "V", 
                    'department' => $row["department"], 
                    'quantity' => $quantity, 
                    'unitPrice' => $unitPrice, 
                    'total' => $total, 
                    'regPrice' => $row["regPrice"], 
                    'scale' => $scale, 
                    'tax' => $row["tax"], 
                    'foodstamp' => $foodstamp, 
                    'discount' => $discount, 
                    'memDiscount' => $memDiscount, 
                    'discountable' => $discountable, 
                    'discounttype' => $discounttype, 
                    'ItemQtty' => $quantity, 
                    'volDiscType' => $volDiscType,
                    'volume' => $volume,
                    'VolSpecial' => $volSpecial, 
                    'mixMatch' => $mixMatch, 
                    'matched' => $matched,
                    'voided' => 1, 
                    'cost' => $cost, 
                    'numflag' => $numflag, 
                    'charflag' => $charflag
                ));

                if ($row["trans_type"] != "T") {
                    $this->session->set("ttlflag",0);
                }

                $this->voidDeposit($upc, $quantity);
            }
        } catch (Exception $ex) {
            if ($ex->getCode() == 0) {
                $json['output'] = $ex->getMessage();
            } elseif ($ex->getCode() == 1) {
                $json['main_frame'] = $ex->getMessage();
            }
        }

        return $json;
    }

    private function voidDeposit($upc, $quantity)
    {
        $dbc = Database::pDataConnect();
        $chk = $dbc->query("SELECT deposit FROM products WHERE upc='$upc'");
        if ($dbc->numRows($chk) > 0) {
            $row = $dbc->fetch_row($chk);
            $dpt = $row['deposit'];
            if ($dpt <= 0) {
                return false; // no deposit found
            }
            $dbc = Database::tDataConnect();
            $dupc = str_pad((int)$dpt,13,'0',STR_PAD_LEFT);
            $transID = $dbc->query(sprintf("SELECT trans_id FROM localtemptrans
                WHERE upc='%s' AND voided=0 AND quantity=%d",
                $dupc,(-1*$quantity)));
            if ($dbc->numRows($transID) > 0) {
                $row = $dbc->fetchRow($transID);
                $transID = $row['trans_id'];
                // pass an empty array instead of $json so
                // voiding the deposit doesn't result in an error
                // message. 
                $this->voidupc((-1*$quantity)."*".$dupc, array(), $transID);

                return true;
            }
        }

        return false;
    }

    public static function messageAndLevel()
    {
        return array(_('Void Limit Exceeded. Login to Continue'), 30);
    }

    public static function adminLoginCallback($success)
    {
        if ($success){
            CoreLocal::set('voidOverride', 1);
            CoreLocal::set('msgrepeat', 1);
            return true;
        }
        CoreLocal::set('voidOverride', 0);

        return false;
    }

    function doc(){
        return "<table cellspacing=0 cellpadding=3 border=1>
            <tr>
                <th>Input</th><th>Result</th>
            </tr>
            <tr>
                <td>VD<i>ringable</i></td>
                <td>Void <i>ringable</i>, which
                may be a product number or an
                open department ring</td>
            </tr>
            </table>";
    }

    /**
      Check if an item is voided or a refund
      @param $num item trans_id in localtemptrans
      @return array of status information with keys:
       - voided (int)
       - scaleprice (numeric)
       - refund (boolean)
       - status (string)
    */
    private function checkstatus($num) 
    {
        $ret = array(
            'voided' => 0,
            'scaleprice' => 0,
            'refund' => false,
            'status' => ''
        );

        $query = "select voided,unitPrice,discountable,
            discounttype,trans_status
            from localtemptrans where trans_id = ".((int)$num);

        $dbc = Database::tDataConnect();
        $result = $dbc->query($query);

        if ($row = $dbc->fetchRow($result)) {

            $ret['voided'] = $row['voided'];
            $ret['scaleprice'] = $row['unitPrice'];

            if ($row["trans_status"] == "V") {
                $ret['status'] = 'V';
            } elseif ($row["trans_status"] == "R") {
                $this->session->set("refund",1);
                $this->session->set("autoReprint",1);
                $ret['status'] = 'R';
                $ret['refund'] = true;
            }
        }
        
        return $ret;
    }
}