CORE-POS/IS4C

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

Summary

Maintainability
F
1 wk
Test Coverage
F
55%
<?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\ItemNotFound;
use COREPOS\pos\lib\MiscLib;
use COREPOS\pos\lib\PrehLib;
use COREPOS\pos\lib\TransRecord;
use COREPOS\pos\lib\Scanning\DiscountType;
use COREPOS\pos\lib\Scanning\PriceMethod;
use COREPOS\pos\lib\Scanning\SpecialDept;
use COREPOS\pos\lib\Scanning\SpecialUPC;
use COREPOS\pos\parser\Parser;
use COREPOS\pos\plugins\Plugin;
use \CoreLocal;
use \DateTime;
use \DateInterval;
use \Exception;

class UPC extends Parser 
{
    /**
      Defines how the UPC was entered.
      Known good values are:
      - keyed
      - scanned
      - macro
      - hid
    */
    private $source = 'keyed';

    const GENERIC_STATUS = 'NA';

    const SCANNED_PREFIX = '0XA';
    const SCANNED_STATUS = 'SS';

    const MACRO_PREFIX = '0XB';
    const MACRO_STATUS = 'KB';

    const HID_PREFIX = '0XC';
    const HID_STATUS = 'HI';

    const GS1_EXPANDED_PREFIX = 'GS1~RX';
    const GS1_EXPANDED_STATUS = 'GS';

    const GS1_ITEM_PREFIX = 'GS1~R4';
    const GS1_ITEM_STATUS = 'NA';

    /**
      The default case is pretty simple. A numeric string
      is checked as a UPC.
      
      The 0XA prefix indicates a scanned value from the scale.
      This prefix was selected because PHP's validation still
      considers the whole string a [hex] number. That helps with
      overall input validation. A complex entry like:
          5*0XA001234567890
      Is handled correctly because there's a "number" on both 
      sides of asterisk. The prefix is then stripped off by
      this parser to get the actual UPC value.

      The GS1~ prefix is an old artificat of wedge compatibility.
      Using something like 0XB instead would probably be
      an improvement.
    */
    public function check($str)
    {
        if (is_numeric($str) && strlen($str) < 16) {
            return true;
        } elseif (is_numeric($str) && substr($str, 0, 4) == '8110') {
            return true;
        } elseif ($this->getPrefix($str) !== false) { 
            return true;
        }

        return false;
    }

    private function prefixes()
    {
        return array(
            self::SCANNED_STATUS => self::SCANNED_PREFIX, 
            self::MACRO_STATUS => self::MACRO_PREFIX, 
            self::HID_STATUS => self::HID_PREFIX, 
            self::GS1_EXPANDED_STATUS => self::GS1_EXPANDED_PREFIX,
            self::GS1_ITEM_STATUS => self::GS1_ITEM_PREFIX,
        );
    }

    private function getPrefix($str)
    {
        foreach ($this->prefixes() as $prefix) {
            $len = strlen($prefix);
            if (substr($str,0,$len) == $prefix && is_numeric(substr($str, $len))) {
                return $prefix;
            }
        }

        return false;
    }

    private function getStatus($source)
    {
        foreach ($this->prefixes() as $status => $prefix) {
            if ($source == $prefix) {
                return $status;
            }
        }

        return self::GENERIC_STATUS;
    }

    function parse($str)
    {
        $this->source = $this->getPrefix($str);
        if ($this->source == self::GS1_ITEM_PREFIX ||
            $this->source == self::GS1_EXPANDED_PREFIX) {
            $str = $this->fixGS1($str);
        }

        /* If fixGS1() found a problem report it and return.
         */
        if (strpos($str,'*PROBLEM') === 0)
        {
            /** $problem
             * [0] '*PROBLEM'
             * [1] A description of the problem
             * [2] The string being parsed at the point the problem was found.
             */
            $problem = explode('|',$str);
            $ret = $this->default_json();
            $problem[1] = str_replace('  ','<br />',$problem[1]);
            $msg = sprintf("%s<br />%s%s", $problem[1], _('Item: '), $problem[2]);
            $ret['output'] = DisplayLib::boxMsg(
                $msg,
                _('DataBar Problem'),
                false,
                DisplayLib::standardClearButton()
            );
            return $ret;
        }

        $this->status = self::GENERIC_STATUS;
        if ($this->source !== false) {
            $this->status = $this->getStatus($this->source);
        }

        /**
          Do not apply scanned items if
          tare has been entered
        */
        if ($this->session->get('tare') > 0 && $this->source === self::SCANNED_PREFIX) {
            return $this->default_json();
        }

        return $this->upcscanned($str);
    }

    private function upcscanned($entered) 
    {
        $ret = $this->default_json();

        $ret = $this->genericSecurity($ret);
        if ($ret['main_frame'] != '') {
            return $ret;
        }

        $upc = $this->sanitizeUPC($entered);

        list($upc,$scaleStickerItem,$scalepriceUPC,$scalepriceEAN) = $this->rewriteScaleSticker($upc);

        $row = $this->lookupItem($upc);

        /* check for special upcs that aren't really products */
        if (!$row) {
            return $this->nonProductUPCs($upc, $ret);
        }

        return $this->handleItem($upc, $row, $scaleStickerItem, $scalepriceUPC, $scalepriceEAN);
    }

    private function handleItem($upc, $row, $scaleStickerItem, $scalepriceUPC, $scalepriceEAN)
    {
        $ret = $this->default_json();
        $myUrl = MiscLib::baseURL();
        $dbc = Database::pDataConnect();

        $quantity = $this->session->get("quantity");
        if ($this->session->get("quantity") == 0 && $this->session->get("multiple") == 0) {
            $quantity = 1;
        }

        /* product exists
           BEGIN error checking round #1
        */

        /**
          If formatted_name is present, copy it directly over
          products.description. This way nothing further down
          the process has to worry about the distinction between
          two potential naming fields.
        */
        if ($row['formatted_name'] != '') {
            $row['description'] = $row['formatted_name'];
        }

        $ret = $this->checkInUse($row, $ret);
        if ($ret['main_frame']) {
            return $ret;
        }

        /**
          Apply special department handlers
          based on item's department
        */
        $deptmods = $this->session->get('SpecialDeptMap');
        if (!is_array($deptmods) && ($this->session->get('NoCompat') == 1 || $dbc->table_exists('SpecialDeptMap'))) {
            $model = new \COREPOS\pos\lib\models\op\SpecialDeptMapModel($dbc);
            $deptmods = $model->buildMap();
            $this->session->set('SpecialDeptMap', $deptmods);
        }
        if (is_array($deptmods) && isset($deptmods[$row['department']])){
            foreach($deptmods[$row['department']] as $mod){
                $obj = SpecialDept::factory($mod, $this->session);
                $ret = $obj->handle($row['department'],$row['normal_price'],$ret);
                if ($ret['main_frame'])
                    return $ret;
            }
        }

        /**
          Detect if a by-weight item has the same weight as the last by-weight
          item. This can indicate a stuck scale.
          The giant if determines whether the item is scalable, that we
          know the weight, and that we know the previous weight (lastWeight)
        
          Pre-weighed items (upc starts with 002) are ignored because they're not
          weighed here. Scalable items that cost one cent are ignored as a special
          case; they're normally entered by keying a quantity multiplier
        */
        if ($this->duplicateWeight($row, $scaleStickerItem)) {
            $this->session->set("strEntered",$row["upc"]);
            $this->session->set("boxMsg",_("<b>Same weight as last item</b>"));
            $this->session->set('boxMsgButtons', array(
                _('Confirm Weight [enter]') => '$(\'#reginput\').val(\'\');submitWrapper();',
                _('Cancel [clear]') => '$(\'#reginput\').val(\'CL\');submitWrapper();',
            ));
            $ret['main_frame'] = $myUrl."gui-modules/boxMsg2.php?quiet=1";
            return $ret;
        }

        if ($row["idEnforced"] > 0){

            if ($this->isDateRestricted($row)) {
                $ret['output'] = DisplayLib::boxMsg(
                    _('product cannot be sold right now'),
                    _('Date Restriction'),
                    false,
                    DisplayLib::standardClearButton()
                );
                return $ret;
            }

            list($badAge, $ret) = PrehLib::ageCheck($row['idEnforced'], $ret);
            if ($badAge === true) {
                return $ret;
            }
        }

        /**
          Apply automatic tare weight
        */
        if ($row['tareweight'] > 0){
            $peek = PrehLib::peekItem();
            if (strstr($peek,"** Tare Weight") === False)
                TransRecord::addTare($row['tareweight']*100);
        } elseif ($row['scale'] != 0 && !$this->session->get("tare") && Plugin::isEnabled('PromptForTare') && !$this->session->get("tarezero")) {
            $ret['main_frame'] = $myUrl.'plugins/PromptForTare/TarePromptInputPage.php?item='.$upc;
            return $ret;
        } else {
            $this->session->set('tarezero', False);
        }

        /* sanity check - ridiculous price 
           (can break db column if it doesn't fit
        */
        if (strlen($row["normal_price"]) > 8){
            $ret['output'] = DisplayLib::boxMsg(
                $upc . '<br />' . _("Claims to be more than $100,000"),
                _('Invalid Item'),
                false,
                DisplayLib::standardClearButton()
            );
            return $ret;
        }

        $scale = ($row["scale"] == 0) ? 0 : 1;
        $qttyEnforced = $row["qttyEnforced"];
        /* use scaleprice bit column to indicate 
           whether values should be interpretted as 
           UPC or EAN */ 
        $scaleprice = ($row['scaleprice'] == 0) ? $scalepriceUPC : $scalepriceEAN;

        /* need a weight with this item
           retry the UPC in a few milliseconds and see
        */
        if ($scale != 0 && $this->session->get("weight") == 0 && $qttyEnforced == 0
            && $this->session->get("quantity") == 0 && !$scaleStickerItem) {

            $this->session->set("SNR",$this->session->get('strEntered'));
            $ret['output'] = DisplayLib::boxMsg(
                _("please put item on scale"),
                _('Weighed Item'),
                true,
                DisplayLib::standardClearButton()
            );
            
            return $ret;
        }

        /* quantity required for this item. Send to
           entry page if one wasn't provided */
        if (($qttyEnforced == 1) && ($this->session->get("multiple") == 0) && ($this->session->get("msgrepeat" == 0) || $this->session->get('qttyvalid') == 0)) {
            $multP = $dbc->prepare("SELECT description, multiple FROM Multiples WHERE upc=?");
            $multR = $dbc->execute($multP, array($row['upc']));
            if ($multR && class_exists('QuickMenus')) {
                $opts = array(
                    'Single - ' . $row['description'] => '1*' . $row['upc'],
                );
                while ($multW = $dbc->fetchRow($multR)) {
                    $opts[$multW['description'] . ' - ' . $row['description']] = $multW['multiple'] . '*' . $row['upc'];
                }
                CoreLocal::set('qmNumber', $opts);
                $plugin_info = new \QuickMenus();
                $ret['main_frame'] = $plugin_info->pluginUrl().'/QMDisplay.php';
                return $ret;
            }
            $ret['main_frame'] = 
                    $myUrl . 'gui-modules/QuantityEntryPage.php'
                    . '?entered-item=' . $this->session->get('strEntered')
                    . '&qty-mode=' . $scale;
            return $ret;
        } 

        /* got a scale weight, make sure the tare
           is valid */
        if ($scale != 0 && !$scaleStickerItem) {
            $quantity = $this->session->get("weight") - $this->session->get("tare");
            if ($this->session->get("quantity") != 0) 
                $quantity = $this->session->get("quantity") - $this->session->get("tare");

            if ($quantity <= 0) {
                $ret['output'] = DisplayLib::boxMsg(
                    _("item weight must be greater than tare weight"),
                    _('Invalid Weight'),
                    false,
                    DisplayLib::standardClearButton()
                );
                return $ret;
            }
            $this->session->set("tare",0);
        }

        /* non-scale items need integer quantities */    
        if ($row["scale"] == 0 && (int) $this->session->get("quantity") != $this->session->get("quantity") ) {
            $ret['output'] = DisplayLib::boxMsg(
                _("fractional quantity cannot be accepted for this item"),
                _('Invalid Quantity'),
                false,
                DisplayLib::standardClearButton()
            );
            return $ret;
        }

        /*
           END error checking round #1
        */    

        // wfc uses deposit field to link another upc
        if (isset($row["deposit"]) && $row["deposit"] > 0){
            $dupc = (int)$row["deposit"];
            $this->addDeposit($dupc);
        }

        $upc = $row["upc"];
        $row['numflag'] = isset($row["local"])?$row["local"]:0;
        $row['description'] = str_replace("'","",$row['description']);

        list($tax, $foodstamp, $discountable) = PrehLib::applyToggles($row['tax'], $row['foodstamp'], $row['discount']);
        $row['tax'] = $tax;
        $row['foodstamp'] = $foodstamp;
        $row['discount'] = $discountable;

        if ($row['special_limit'] && $row['discounttype'] == 0 && $this->hardLimit($dbc, $row, $quantity)) {
            $ret['output'] = DisplayLib::boxMsg(
                _("Already purchase maximum amount"),
                _('Quantity Limited Item'),
                false,
                DisplayLib::standardClearButton()
            );
            return $ret;
        }
        $row = $this->enforceSaleLimit($dbc, $row, $quantity);

        /*
            BEGIN: figure out discounts by type
        */

        $discountObject = DiscountType::getObject($row['discounttype'], $this->session);

        /* add in sticker price and calculate a quantity
           if the item is stickered, scaled, and on sale. 

           otherwise, if the item is sticked, scaled, and
           not on sale but has a non-zero price attempt
           to calculate a quantity. this makes the quantity
           field more consistent for reporting purposes.
           however, if the calculated quantity somehow
           introduces a rounding error fall back to the
           sticker's price. for non-sale items, the price
           the customer pays needs to match the sticker
           price exactly.

           items that are not scaled do not need a fractional
           quantity and items that do not have a normal_price
           assigned cannot calculate a proper quantity.
        */
        $rawQty = false;
        if ($scaleStickerItem) {
            if ($discountObject->isSale() && $scale == 1 && $row['normal_price'] != 0 && $this->session->get('VariableNoSalePrice') != 1) {
                $quantity = MiscLib::truncate2($scaleprice / $row["normal_price"]);
            } elseif ($scale == 1 && $row['normal_price'] != 0) {
                $quantity = MiscLib::truncate2($scaleprice / $row["normal_price"]);
                if (round($scaleprice, 2) != round($quantity * $row['normal_price'], 2)) {
                    $quantity = 1.0;
                    $row['normal_price'] = $scaleprice;
                    $rawQty = $scaleprice / $row['normal_price'];
                    $row['cost'] = MiscLib::truncate2($row['cost'] * $rawQty);
                } 
            } else {
                $row['normal_price'] = $scaleprice;
            }
        }

        /*
            END: figure out discounts by type
        */

        $row['trans_subtype'] = $this->status;
        $pricemethod = MiscLib::nullwrap($discountObject->isSale() ? $row['specialpricemethod'] : $row["pricemethod"]);
        $priceMethodObject = PriceMethod::getObject($pricemethod, $this->session);

        // prefetch: otherwise object members 
        // pass out of scope in addItem()
        $prefetch = $discountObject->priceInfo($row,$quantity);
        $added = $priceMethodObject->addItem($row, $quantity, $discountObject);

        if (!$added) {
            $ret['output'] = DisplayLib::boxMsg(
                $priceMethodObject->errorInfo(),
                '',
                false,
                DisplayLib::standardClearButton()
            );
            return $ret;
        } elseif (false && $rawQty) { // rewrite edge-case quantities idea
            $dbc = Database::tDataConnect();
            $prep = $dbc->prepare('SELECT MAX(trans_id) FROM localtemptrans WHERE upc=?');
            $tID = $dbc->getValue($prep, array($row['upc']));
            if ($tID) {
                $prep = $dbc->prepare('UPDATE localtemptrans SET quantity=?, ItemQtty=? WHERE trans_id=?');
                $res = $dbc->execute($prep, array($rawQty, $rawQty, $tID));
            }
        }

        /* add discount notifications lines, if applicable */
        $discountObject->addDiscountLine();

        // cleanup, reset flags and beep
        if ($quantity != 0) {

            $this->session->set("msgrepeat",0);
            $this->session->set("qttyvalid",0);

            $ret['udpmsg'] = 'goodBeep';
        }

        /* reset various flags and variables */
        if ($this->session->get("tare") != 0) $this->session->set("tare",0);
        $this->session->set("ttlflag",0);
        $this->session->set("fntlflag",0);
        $this->session->set("quantity",0);
        $this->session->set("itemPD",0);
        Database::setglobalflags(0);

        /* output item list, update totals footer */
        $ret['redraw_footer'] = True;
        $ret['output'] = DisplayLib::lastpage();

        if ($prefetch['unitPrice']==0 && $row['discounttype'] == 0){
            $ret['main_frame'] = $myUrl.'gui-modules/priceOverride.php';
        }

        return $ret;
    }

    private function addDeposit($upc)
    {
        $upc = str_pad($upc,13,'0',STR_PAD_LEFT);

        $dbc = Database::pDataConnect();
        $query = "select description,scale,tax,foodstamp,discounttype,
            discount,department,normal_price
                   from products where upc='".$upc."'";
        $result = $dbc->query($query);

        if ($dbc->num_rows($result) <= 0) return;

        $row = $dbc->fetchRow($result);
        
        $description = $row["description"];
        $description = str_replace("'", "", $description);
        $description = str_replace(",", "", $description);

        $scale = 0;
        if ($row["scale"] != 0) $scale = 1;

        list($tax, $foodstamp, $discountable) = PrehLib::applyToggles($row['tax'], $row['foodstamp'], $row['discount']);

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

        $quantity = 1;
        if ($this->session->get("quantity") != 0) {
            $quantity = $this->session->get("quantity");
        }

        $saveRefund = $this->session->get("refund");

        TransRecord::addRecord(array(
            'upc' => $upc,
            'description' => $description,
            'trans_type' => 'I',
            'trans_subtype' => 'AD',
            'department' => $row['department'],
            'quantity' => $quantity,
            'ItemQtty' => $quantity,
            'unitPrice' => $row['normal_price'],
            'total' => $quantity * $row['normal_price'],
            'regPrice' => $row['normal_price'],
            'scale' => $scale,
            'tax' => $tax,
            'foodstamp' => $foodstamp,
            'discountable' => $discountable,
            'discounttype' => $discounttype,
        ));

        $this->session->set("refund",$saveRefund);
    }

    function fixGS1($str) {
        // remove GS1~ prefix
        $str = substr($str, 6);

        // check application identifier

        // coupon; return whole thing
        if (substr($str,0,4) == "8110")
            return $str;

        /* GTIN-14; return w/ or w/o check digit,
         * ignore any other fields for now
         * There are cases where the first digit after
         *  the AI is not 0 and so the with check digit
         *  will lose that and the lookup to products fail.
         * Trap cases where keeping the check digit and
         *  the first digit after the AI is not 0.
         */
        if (substr($str,0,2) == "01") {
            if ($this->session->get("UpcIncludeCheckDigits") &&
                    $str[2] != '0') {
                return sprintf("%s|%s|%s", '*PROBLEM',
                    _("Cannot handle this item ID.  Perhaps try the PLU on the sticker."),
                    substr($str,2));
            }
            return substr($str,
                ($this->session->get("UpcIncludeCheckDigits")) ? 3 : 2,
                13);
        }
        
        // application identifier not recognized
        // will likely cause no such item error
        return $str; 
    }

    public function expandUPCE($entered)
    {
        $par6 = substr($entered, -1);
        if ($par6 == 0) $entered = substr($entered, 0, 3)."00000".substr($entered, 3, 3);
        elseif ($par6 == 1) $entered = substr($entered, 0, 3)."10000".substr($entered, 3, 3);
        elseif ($par6 == 2) $entered = substr($entered, 0, 3)."20000".substr($entered, 3, 3);
        elseif ($par6 == 3) $entered = substr($entered, 0, 4)."00000".substr($entered, 4, 2);
        elseif ($par6 == 4) $entered = substr($entered, 0, 5)."00000".substr($entered, 5, 1);
        else $entered = substr($entered, 0, 6)."0000".$par6;

        return $entered;
    }

    public function sanitizeUPC($entered)
    {
        // leading/trailing whitespace creates issues
        $entered = trim($entered);

        // leave GS1 barcodes alone otherwise
        if ($this->source == self::GS1_ITEM_PREFIX ||
            $this->source == self::GS1_EXPANDED_PREFIX) {
            return $entered;
        }
        if (substr($entered, 0, 4) == '8110' && strlen($entered) > 16) {
            return $entered;
        }

        /* exapnd UPC-E */
        if (substr($entered, 0, 1) == 0 && strlen($entered) == 7) {
            $entered = $this->expandUPCE($entered);
        }

        /* make sure upc length is 13 */
        $upc = "";
        if ($this->session->get('EanIncludeCheckDigits') != 1) {
            /** 
              If EANs do not include check digits, the value is 13 digits long,
              and the value does not begin with a zero, it most likely
              represented a hand-keyed EAN13 value w/ check digit. In this configuration
              it's probably a miskey so trim the last digit.
            */
            if (strlen($entered) == 13 && substr($entered, 0, 1) != 0) {
                $upc = "0".substr($entered, 0, 12);
            }
        }
        // pad value to 13 digits
        $upc = substr("0000000000000".$entered, -13);

        return $upc;
    }

    /* extract scale-sticker prices 
       Mixed check digit settings do not work here. 
       Scale UPCs and EANs must uniformly start w/
       002 or 02.
       @return [array] 
        boolean is-scale-sticker
        number UPC-price
        number EAN-price
    */
    private function rewriteScaleSticker($upc)
    {
        if ($upc == '0028491108110' || $upc == '0028491108310' || $upc == '0028491108010' || $upc == '0028491108210') {
            return array($upc, false, 0, 0);
        }
        $scalePrefix = '002';
        $scaleStickerItem = false;
        $scaleCheckDigits = false;
        if ($this->session->get('UpcIncludeCheckDigits') == 1) {
            $scalePrefix = '02';
            $scaleCheckDigits = true;
        }
        $scalepriceUPC = 0;
        $scalepriceEAN = 0;
        // prefix indicates it is a scale-sticker
        if (substr($upc, 0, strlen($scalePrefix)) == $scalePrefix) {
            $scaleStickerItem = true;
            // extract price portion of the barcode
            // position varies depending whether a check
            // digit is present in the upc
            $scalepriceUPC = MiscLib::truncate2(substr($upc, -4)/100);
            $scalepriceEAN = MiscLib::truncate2(substr($upc, -5)/100);
            if ($scaleCheckDigits) {
                $scalepriceUPC = MiscLib::truncate2(substr($upc, 8, 4)/100);
                $scalepriceEAN = MiscLib::truncate2(substr($upc, 7, 5)/100);
            }
            $rewriteClass = $this->session->get('VariableWeightReWriter');
            if ($rewriteClass != '' && substr($rewriteClass, 0, 7) !== 'COREPOS' && class_exists('COREPOS\\pos\\lib\\Scanning\\VariableWeightReWrites\\' . $rewriteClass)) {
                $rewriteClass = 'COREPOS\\pos\\lib\\Scanning\\VariableWeightReWrites\\' . $rewriteClass;
            } elseif ($rewriteClass === '' || !class_exists($rewriteClass)) {
                $rewriteClass = 'COREPOS\\pos\\lib\\Scanning\\VariableWeightReWrites\\ZeroedPriceReWrite';
            }
            $rewriter = new $rewriteClass();
            $upc = $rewriter->translate($upc, $scaleCheckDigits);
            // I think this is WFC special casing; needs revising.
            if ($upc == "0020006000000" || $upc == "0020010000000") $scalepriceUPC *= -1;
        }

        return array($upc, $scaleStickerItem, $scalepriceUPC, $scalepriceEAN);
    }

    public static $requestInfoHeader = 'customer age';
    public static $requestInfoMsg = 'Type customer birthdate YYYYMMDD';
    public static function requestInfoCallback($info)
    {
        if ((is_numeric($info) && strlen($info)==8) || $info == 1){
            CoreLocal::set("memAge",$info);
            $inp = urlencode(CoreLocal::get('strEntered'));
            return MiscLib::baseURL() . 'gui-modules/pos2.php?reginput=' . $inp . '&repeat=1';
        }
        return False;
    }
    public static function requestInfoErrorMsg()
    {
        try {
            $birthDateYMD = CoreLocal::get('memAge');
            if (CoreLocal::get('AgeMode') == 'mdY') {
                $birthDateYMD = substr(CoreLocal::get('memAge'), -4) . substr(CoreLocal::get('memAge'), 0, 4);
            }
            $ofAgeOnDay = new DateTime($birthDateYMD);
            $ofAgeOnDay->add(new DateInterval("P21Y"));
        } catch (Exception $ex) {
            $ofAgeOnDay = new DateTime(date('Y-m-d', strtotime('tomorrow')));
            return 'Invalid entry';
        }

        return 'Of age on ' . $ofAgeOnDay->format('m/d/y');
    }

    private function genericSecurity($ret)
    {
        $myUrl = MiscLib::baseURL();
        /* force cashiers to enter a comment on refunds */
        if ($this->session->get("refund")==1 && $this->session->get("refundComment") == ""){
            $ret['udpmsg'] = 'twoPairs';
            $ret['main_frame'] = $myUrl.'gui-modules/refundComment.php';
            if ($this->session->get("SecurityRefund") > 20){
                $ret['main_frame'] = $myUrl."gui-modules/adminlogin.php?class=COREPOS-pos-lib-adminlogin-RefundAdminLogin";
            }
            $this->session->set("refundComment",$this->session->get("strEntered"));
            return $ret;
        }
        if ($this->session->get('itemPD') > 0 && $this->session->get('SecurityLineItemDiscount') == 30 && $this->session->get('msgrepeat')==0){
            $ret['main_frame'] = $myUrl."gui-modules/adminlogin.php?class=COREPOS-pos-lib-adminlogin-LineItemDiscountAdminLogin";
            return $ret;
        }

        return $ret;
    }

    private function duplicateWeight($row, $scaleStickerItem)
    {
        if ($row['scale'] == 1 
            && $this->session->get("lastWeight") > 0 && $this->session->get("weight") > 0
            && abs($this->session->get("weight") - $this->session->get("lastWeight")) < 0.0005
            && !$scaleStickerItem && abs($row['normal_price']) > 0.01
            && $this->session->get('msgrepeat') == 0) {
            return true;
        }

        return false;
    }

    private function isDateRestricted($row)
    {
        $dbc = Database::pDataConnect();
        $restrictQ = "SELECT upc,dept_ID FROM dateRestrict WHERE
            ( upc='{$row['upc']}' AND
              ( ".$dbc->datediff($dbc->now(),'restrict_date')."=0 OR
                ".$dbc->dayofweek($dbc->now())."=restrict_dow
              ) AND
              ( (restrict_start IS NULL AND restrict_end IS NULL) OR
                ".$dbc->curtime()." BETWEEN restrict_start AND restrict_end
              )
             ) OR 
            ( dept_ID='{$row['department']}' AND
              ( ".$dbc->datediff($dbc->now(),'restrict_date')."=0 OR
                ".$dbc->dayofweek($dbc->now())."=restrict_dow
              ) AND
              ( (restrict_start IS NULL AND restrict_end IS NULL) OR
                ".$dbc->curtime()." BETWEEN restrict_start AND restrict_end
              )
            )";
        $restrictR = $dbc->query($restrictQ);
        return $dbc->numRows($restrictR) > 0 ? true : false;
    }

    private function nonProductUPCs($upc, $ret)
    {
        $dbc = Database::pDataConnect();
        $objs = is_array($this->session->get("SpecialUpcClasses")) ? $this->session->get('SpecialUpcClasses') : array();
        foreach($objs as $className){
            $instance = SpecialUPC::factory($className, $this->session);
            if ($instance->isSpecial($upc)){
                return $instance->handle($upc,$ret);
            }
        }

        // no match; not a product, not special
        if ($this->session->get('NoCompat') == 1 || $dbc->table_exists('IgnoredBarcodes')) {
            // lookup UPC in tabe of ignored barcodes
            // this just suppresses any error message from
            // coming back
            $query = 'SELECT upc FROM IgnoredBarcodes WHERE upc=\'' . $upc . "'";
            $result = $dbc->query($query);
            if ($result && $dbc->num_rows($result)) {
                return $this->default_json();
            }
        }
        
        $obj = ItemNotFound::factory($this->session->get('ItemNotFound'));
        $ret = $obj->handle($upc, $ret);

        return $ret;
    }

    private function lookupItem($upc)
    {
        $dbc = Database::pDataConnect();
        $query = "SELECT inUse,upc,description,normal_price,scale,deposit,
            qttyEnforced,department,local,tax,foodstamp,discount,
            discounttype,specialpricemethod,special_price,groupprice,
            pricemethod,quantity,specialgroupprice,specialquantity,
            mixmatchcode,idEnforced,tareweight,scaleprice";
        if ($this->session->get('NoCompat') == 1) {
            $query .= ', 
                line_item_discountable, 
                formatted_name, 
                special_limit,
                CASE 
                    WHEN received_cost <> 0 AND received_cost IS NOT NULL
                        THEN received_cost
                    WHEN discounttype > 0 AND special_cost <> 0 AND special_cost IS NOT NULL 
                        THEN special_cost 
                    ELSE cost END AS cost';
        } else {
            $table = $dbc->tableDefinition('products');
            // New column 16Apr14
            if (isset($table['line_item_discountable'])) {
                $query .= ', line_item_discountable';
            } else {
                $query .= ', 1 AS line_item_discountable';
            }
            // New column 16Apr14
            if (isset($table['formatted_name'])) {
                $query .= ', formatted_name';
            } else {
                $query .= ', \'\' AS formatted_name';
            }
            // New column 25Nov14
            if (isset($table['special_limit'])) {
                $query .= ', special_limit';
            } else {
                $query .= ', 0 AS special_limit';
            }
            // New column 20Oct16
            if (isset($table['special_cost']) && isset($table['received_cost'])) {
                $query .= ', CASE WHEN received_cost <> 0 AND received_cost IS NOT NULL THEN received_cost
                    WHEN discounttype > 0 AND special_cost <> 0 AND special_cost IS NOT NULL 
                    THEN special_cost ELSE cost END AS cost';
            } else {
                $query .= ', cost';
            }
        }
        $query .= " FROM products WHERE upc = '".$upc."'";
        $result = $dbc->query($query);
        $row = $dbc->fetchRow($result);

        return $row;
    }

    private function checkInUse($row, $ret)
    {
        /* Implementation of inUse flag
         *   if the flag is not set and it's likely a keyed item
         *   display a warning dialog noting this
         *   and allowing the sale to be confirmed or canceled
         */
        if ($row["inUse"] == 0) {
            TransRecord::addLogRecord(array(
                'upc' => $row['upc'],
                'description' => $row['description'],
                'department' => $row['department'],
                'charflag' => 'IU',
            ));
            if (substr($row['upc'], 0, 6) == '000000' && $this->session->get('msgrepeat') == 0) {
                $this->session->set("strEntered",$row["upc"]);
                $this->session->set("boxMsg", _("Inactive PLU. Is this the correct item: ") . $row['description']);
                $this->session->set('boxMsgButtons', array(
                    _('Confirm Sale [enter]') => '$(\'#reginput\').val(\'\');submitWrapper();',
                    _('Cancel [clear]') => '$(\'#reginput\').val(\'CL\');submitWrapper();',
                ));
                $ret['main_frame'] = MiscLib::baseURL() . "gui-modules/boxMsg2.php?quiet=1";
            }
        }

        return $ret;
    }

    private function hardLimit($dbc, $row, $quantity)
    {
            $appliedQ = "
                SELECT SUM(quantity) AS saleQty
                FROM " . $this->session->get('tDatabase') . $dbc->sep() . "localtemptrans
                WHERE discounttype = 0
                    AND (
                        upc='{$row['upc']}'
                        OR (mixMatch='{$row['mixmatchcode']}' AND mixMatch<>''
                            AND mixMatch<>'0' AND mixMatch IS NOT NULL)
                    )";
            $appliedR = $dbc->query($appliedQ);
            if ($appliedR && $dbc->num_rows($appliedR)) {
                $appliedW = $dbc->fetch_row($appliedR);
                if (($appliedW['saleQty']+$quantity) > $row['special_limit']) {
                    return true;
                }
            }

            return false;
    }

    private function enforceSaleLimit($dbc, $row, $quantity)
    {
        if ($row['specialpricemethod'] == 7) {
            return $row;
        }
        /**
          Enforce per-transaction sale limits
        */
        if ($row['special_limit'] > 0) {
            $appliedQ = "
                SELECT SUM(quantity) AS saleQty
                FROM " . $this->session->get('tDatabase') . $dbc->sep() . "localtemptrans
                WHERE discounttype <> 0
                    AND (
                        upc='{$row['upc']}'
                        OR (mixMatch='{$row['mixmatchcode']}' AND mixMatch<>''
                            AND mixMatch<>'0' AND mixMatch IS NOT NULL
                            AND upc <> 'ITEMDISCOUNT')
                    )";
            $appliedR = $dbc->query($appliedQ);
            if ($appliedR && $dbc->num_rows($appliedR)) {
                $appliedW = $dbc->fetch_row($appliedR);
                if (($appliedW['saleQty']+$quantity) > $row['special_limit']) {
                    $row['discounttype'] = 0;
                    $row['special_price'] = 0;
                    $row['specialpricemethod'] = 0;
                    $row['specialquantity'] = 0;
                    $row['specialgroupprice'] = 0;
                }
            }
        }

        return $row;
    }

    function doc(){
        return "<table cellspacing=0 cellpadding=3 border=1>
            <tr>
                <th>Input</th><th>Result</th>
            </tr>
            <tr>
                <td><i>product number</i></td>
                <td>Try to ring up the specified product.
                Coupon handling is included here</td>
            </tr>
            </table>";
    }
}