/** Add or update a discount module in the current transaction. Automatically subtotals if the discount changes. */ public static function updateDiscount(DiscountModule $mod, $do_subtotal = true) { $changed = true; // serialize/unserialize before saving to avoid // auto-session errors w/ undefined classes $current_discounts = unserialize(CoreLocal::get('CurrentDiscounts')); if (!is_array($current_discounts)) { $current_discounts = array(); } /** Examine current discounts to see if this one has already applied */ foreach ($current_discounts as $class => $obj) { if ($mod->name == $obj->name) { if ($mod->percentage() == $obj->percentage()) { $changed = false; } break; } } if ($changed) { /** Add object to the list of active discounts Then loop through to see whether it changes the effective discount with stacking settings taken into account */ $current_discounts[$mod->name] = $mod; $old_effective_discount = CoreLocal::get('percentDiscount'); $new_effective_discount = 0; foreach ($current_discounts as $obj) { if (CoreLocal::get('NonStackingDiscounts') && $obj->percentage() > $new_effective_discount) { $new_effective_discount = $obj->percentage(); } elseif (CoreLocal::get('NonStackingDiscounts') == 0) { $new_effective_discount += $obj->percentage(); } } /** When discount changes: 1. Update the session value 2. Update the localtemptrans.percentDiscount value 3. Subtotal the transaction */ if ($old_effective_discount != $new_effective_discount) { CoreLocal::set('percentDiscount', $new_effective_discount); $dbc = Database::tDataConnect(); $dbc->query('UPDATE localtemptrans SET percentDiscount=' . (int) $new_effective_discount); if ($do_subtotal) { PrehLib::ttl(); } } // serialize/unserialize before saving to avoid // auto-session errors w/ undefined classes CoreLocal::set('CurrentDiscounts', serialize($current_discounts)); } }
public function calculate($discountable_total = 0) { $discount = parent::calculate(); if (CoreLocal::get('NeedDiscountFlag') === 1) { $extra = 0.05 * CoreLocal::get('discountableTotal'); $discount = MiscLib::truncate2($discount + $extra); } return $discount; }
function parse($str) { $ret = $this->default_json(); if (CoreLocal::get('isMember') !== 1) { $ret['output'] = DisplayLib::boxMsg(_("Apply member number first"), _('No member selected'), false, array_merge(array('Member Search [ID]' => 'parseWrapper(\'ID\');'), DisplayLib::standardClearButton())); return $ret; } elseif (CoreLocal::get('NeedDiscountFlag') == 1) { $ret['output'] = DisplayLib::boxMsg(_("discount already applied"), '', false, DisplayLib::standardClearButton()); return $ret; } else { CoreLocal::set('NeedDiscountFlag', 1); $NBDisc = CoreLocal::get('needBasedPercent') * 100; DiscountModule::updateDiscount(new DiscountModule($NBDisc, 'NeedBasedDiscount')); $ret['output'] = DisplayLib::lastpage(); $ret['redraw_footer'] = true; return $ret; } }
/** Assign a member number to a transaction @param $member CardNo from custdata @param $personNumber personNum from custdata See memberID() for more information. */ public static function setMember($member, $personNumber, $row = array()) { $conn = Database::pDataConnect(); /** Look up the member information here. There's no good reason to have calling code pass in a specially formatted row of data */ $query = "\n SELECT \n CardNo,\n personNum,\n LastName,\n FirstName,\n CashBack,\n Balance,\n Discount,\n ChargeOk,\n WriteChecks,\n StoreCoupons,\n Type,\n memType,\n staff,\n SSI,\n Purchases,\n NumberOfChecks,\n memCoupons,\n blueLine,\n Shown,\n id \n FROM custdata \n WHERE CardNo = " . (int) $member . "\n AND personNum = " . (int) $personNumber; $result = $conn->query($query); $row = $conn->fetch_row($result); CoreLocal::set("memberID", $member); CoreLocal::set("memType", $row["memType"]); CoreLocal::set("lname", $row["LastName"]); CoreLocal::set("fname", $row["FirstName"]); CoreLocal::set("Type", $row["Type"]); CoreLocal::set("isStaff", $row["staff"]); CoreLocal::set("SSI", $row["SSI"]); if (CoreLocal::get("Type") == "PC") { CoreLocal::set("isMember", 1); } else { CoreLocal::set("isMember", 0); } /** Optinonally use memtype table to normalize attributes by member type */ if (CoreLocal::get('useMemTypeTable') == 1 && $conn->table_exists('memtype')) { $prep = $conn->prepare('SELECT discount, staff, ssi FROM memtype WHERE memtype=?'); $res = $conn->execute($prep, array((int) CoreLocal::get('memType'))); if ($conn->num_rows($res) > 0) { $mt_row = $conn->fetch_row($res); $row['Discount'] = $mt_row['discount']; CoreLocal::set('isStaff', $mt_row['staff']); CoreLocal::set('SSI', $mt_row['ssi']); } } if (CoreLocal::get("isStaff") == 0) { CoreLocal::set("staffSpecial", 0); } /** Determine what string is shown in the upper left of the screen to indicate the current member */ $memMsg = '#' . $member; if (!empty($row['blueLine'])) { $memMsg = $row['blueLine']; } $chargeOk = self::chargeOk(); if (CoreLocal::get("balance") != 0 && $member != CoreLocal::get("defaultNonMem")) { $memMsg .= _(" AR"); } if (CoreLocal::get("SSI") == 1) { $memMsg .= " #"; } if ($conn->tableExists('CustomerNotifications')) { $blQ = ' SELECT message FROM CustomerNotifications WHERE cardNo=' . (int) $member . ' AND type=\'blueline\' ORDER BY message'; $blR = $conn->query($blQ); while ($blW = $conn->fetchRow($blR)) { $memMsg .= ' ' . $blW['message']; } } CoreLocal::set("memMsg", $memMsg); self::setAltMemMsg(CoreLocal::get("store"), $member, $personNumber, $row, $chargeOk); /** Set member number and attributes in the current transaction */ $conn2 = Database::tDataConnect(); $memquery = "\n UPDATE localtemptrans \n SET card_no = '" . $member . "',\n memType = " . sprintf("%d", CoreLocal::get("memType")) . ",\n staff = " . sprintf("%d", CoreLocal::get("isStaff")); $conn2->query($memquery); /** Add the member discount */ if (CoreLocal::get('discountEnforced')) { // skip subtotaling automatically since that occurs farther down DiscountModule::updateDiscount(new DiscountModule($row['Discount'], 'custdata'), false); } /** Log the member entry */ CoreLocal::set("memberID", $member); $opts = array('upc' => 'MEMENTRY', 'description' => 'CARDNO IN NUMFLAG', 'numflag' => $member); TransRecord::add_log_record($opts); /** Optionally add a subtotal line depending on member_subtotal setting. */ if (CoreLocal::get('member_subtotal') === 0 || CoreLocal::get('member_subtotal') === '0') { $noop = ""; } else { self::ttl(); } }
/** Initialize transaction variable in session. This function is called after the end of every transaction so these values will be the the defaults every time. */ public static function transReset() { /** @var End Indicates transaction has ended 0 => transaction in progress 1 => transaction is complete */ CoreLocal::set("End", 0); /** @var memberID Current member number */ CoreLocal::set("memberID", "0"); /** @var TaxExempt Tax exempt status flag 0 => transaction is taxable 1 => transaction is tax exempt */ CoreLocal::set("TaxExempt", 0); /** @var yousaved Total savings on the transaction (as float). Includes any if applicable: - transaction level percent discount - sale prices (localtemptrans.discount) - member prices (localtemptrans.memDiscount) */ CoreLocal::set("yousaved", 0); /** @var couldhavesaved Total member savings that were not applied. Consists of localtemptrans.memDiscount on non-member purchases */ CoreLocal::set("couldhavesaved", 0); /** @var specials Total saving via sale prices. Consists of localtemptrans.discount and when applicable localtemptrans.memDiscount */ CoreLocal::set("specials", 0); /** @var tare Current tare setting (as float) */ CoreLocal::set("tare", 0); /** @var change Amount of change due (as float) */ CoreLocal::set("change", 0); /** @var toggletax Alter the next item's tax status - 0 => do nothing - 1 => change next tax status */ CoreLocal::set("toggletax", 0); /** @var togglefoodstamp Alter the next item's foodstamp status - 0 => do nothing - 1 => change next foodstamp status */ CoreLocal::set("togglefoodstamp", 0); /** @var toggleDiscountable Alter the next item's discount status - 0 => do nothing - 1 => change next discount status */ CoreLocal::set("toggleDiscountable", 0); /** @var refund Indicates current ring is a refund. This is set as a session variable as it could apply to items, open rings, or potentially other kinds of input. - 0 => not a refund - 1 => refund */ CoreLocal::set("refund", 0); /** @var casediscount Line item case discount percentage (as integer; 5 = 5%). This feature may be redundant in that it could be handled with the generic line-item discount. It more or less just differs in that the messages say "Case". */ CoreLocal::set("casediscount", 0); /** @var multiple Cashier used the "*" key to enter a multiplier. This currently makes the products.qttyEnforced flag work. This may be redundant and the quantity setting below is likely sufficient to determine whether a multiplier was used. */ CoreLocal::set("multiple", 0); /** @var quantity Quantity for the current ring. A non-zero value usually means the cashier used "*" to enter a multiplier. A value of zero gets converted to one unless the item requires a quantity via products.scale or products.qttyEnforced. */ CoreLocal::set("quantity", 0); /** @var strEntered Stores the last user input from the main POS screen. Used in conjunction with the msgrepeat option. */ CoreLocal::set("strEntered", ""); /** @var strRemembered Value to use as input the next time the main POS screen loads. Used in conjunction with the msgrepeat option. */ CoreLocal::set("strRemembered", ""); /** @var msgrepeat Controls repeat input behavior - 0 => do nothing - 1 => set POS input to the value in strRemembered strEntered, strRemembered, and msgrepeat are strongly interrelated. When parsing user input on the main POS screen, the entered value is always stored as strEntered. msgrepeat gets used in two slightly different ways. If you're on a page other than the main screen, set msgrepeat to 1 and strRemembered to the desired input, then redirect to pos2.php. This will run the chosen value through standard input processing. The other way msgrepeat is used is with boxMsg2.php. This page is a generic enter to continue, clear to cancel prompt. If you redirect to boxMsg2.php and the user presses enter, POS will set msgrepeat to 1 and copy strEntered into strRemembered effectively repeating the last input. Code using this feature will interpret a msgrepeat value of 1 to indicate the user has given confirmation. msgrepeat is always cleared back to zero when input processing finishes. */ CoreLocal::set("msgrepeat", 0); /** @var lastRepeat [Optional] Reason for the last repeated message Useful to set & check in situations where multiple confirmations may be required. */ CoreLocal::set('lastRepeat', ''); /** @var boxMsg Message string to display on the boxMsg2.php page */ CoreLocal::set("boxMsg", ""); /** @var itemPD Line item percent discount (as integer; 5 = 5%). Applies a percent discount to the current ring. */ CoreLocal::set("itemPD", 0); /** @var cashierAgeOverride This flag indicates a manager has given approval for the cashier to sell age-restricted items. This setting only comes into effect if the cashier is too young. The value persists for the remainder of the transaction so the manager does not have to give approval for each individual item. - 0 => no manager approval - 1 => manager has given approval */ CoreLocal::set("cashierAgeOverride", 0); /** @var voidOverride This flag indicates a manager has given approval for the cashier to void items beyond the per transaction limit. The value persists for the remainder of the transaction so the manager does not have to give approval for each individual item. - 0 => no manager approval - 1 => manager has given approval */ CoreLocal::set("voidOverride", 0); /** @var lastWeight The weight of the last by-weight item entered into the transaction. It's used to monitor for scale problems. Consecutive items with the exact same weight often indicate the scale is stuck or not responding properly. */ CoreLocal::set("lastWeight", 0.0); /** @var CachePanEncBlcok Stores the encrypted string of card information provided by the CC terminal. If the terminal is facing the customer, the customer may swipe their card before the cashier is done ringing in items so the value is stored in session until the cashier is ready to process payment */ CoreLocal::set("CachePanEncBlock", ""); /** @var CachePinEncBlock Stores the encrypted string of PIN data. Similar to CachePanEncBlock. */ CoreLocal::set("CachePinEncBlock", ""); /** @var CacheCardType Stores the selected card type. Similar to CachePanEncBlock. Known values are: - CREDIT - DEBIT - EBTFOOD - EBTCASH */ CoreLocal::set("CacheCardType", ""); /** @var CacheCardCashBack Stores the select cashback amount. Similar to CachePanEncBlock. */ CoreLocal::set("CacheCardCashBack", 0); /** @var ccTermState Stores a string representing the CC terminals current display. This drives an optional on-screen icon to let the cashier know what the CC terminal is doing if they cannot see its screen. */ CoreLocal::set('ccTermState', 'swipe'); /** @var paycard_voiceauthcode Stores a voice authorization code for use with a paycard transaction. Not normally used but required to pass Mercury's certification script. */ CoreLocal::set("paycard_voiceauthcode", ""); /** @var ebt_authcode Stores a foodstamp authorization code. Similar to paycard_voiceauthcode. */ CoreLocal::set("ebt_authcode", ""); /** @var ebt_vnum Stores a foodstamp voucher number. Similar to paycard_voiceauthcode. */ CoreLocal::set("ebt_vnum", ""); /** @var paycard_keyed - True => card number was hand keyed - False => card was swiped Normally POS figures this out automatically but it has to be overriden to pass Mercury's certification script. They require some keyed transactions even though the CC terminal is only capable of producing swipe-style data. */ CoreLocal::set("paycard_keyed", False); if (!is_array(CoreLocal::get('PluginList'))) { CoreLocal::set('PluginList', array()); } if (is_array(CoreLocal::get('PluginList'))) { foreach (CoreLocal::get('PluginList') as $p) { if (!class_exists($p)) { continue; } $obj = new $p(); $obj->plugin_transaction_reset(); } } if (is_array(CoreLocal::get('Notifiers'))) { foreach (CoreLocal::get('Notifiers') as $n) { if (!class_exists($n)) { continue; } $obj = new $n(); $obj->transactionReset(); } } FormLib::clearTokens(); DiscountModule::transReset(); }
/** Finish the current transaction @param $incomplete [boolean] optional, default false This method: 1) Adds tax and discount lines if transaction is complete (i.e., $incomplete == false) 2) Rotates data out of localtemptrans 3) Advances trans_no variable to next available value This method replaces older ajax-end.php / end.php operations where the receipt was printed first and then steps 1-3 above happened. This method should be called BEFORE printing a receipt. Receipts are now always printed via localtranstoday. */ public static function finalizeTransaction($incomplete = false) { if (!$incomplete) { self::addtransDiscount(); self::addTax(); $taxes = Database::LineItemTaxes(); foreach ($taxes as $tax) { if (CoreLocal::get('TaxExempt') == 1) { $tax['amount'] = 0.0; } self::addLogRecord(array('upc' => 'TAXLINEITEM', 'description' => $tax['description'], 'numflag' => $tax['rate_id'], 'amount2' => $tax['amount'])); } DiscountModule::lineItems(); } if (Database::rotateTempData()) { // rotate data Database::clearTempTables(); } // advance trans_no value Database::loadglobalvalues(); $nextTransNo = Database::gettransno(CoreLocal::get('CashierNo')); CoreLocal::set('transno', $nextTransNo); Database::setglobalvalue('TransNo', $nextTransNo); }
/** Get information about how much the coupon is worth @param $id [int] coupon ID @return array with keys: value => [float] coupon value department => [int] department number for the coupon description => [string] description for coupon */ public function getValue($id) { $infoW = $this->lookupCoupon($id); if ($infoW === false) { return array('value' => 0, 'department' => 0, 'description' => ''); } $transDB = Database::tDataConnect(); /* if we got this far, the coupon * should be valid */ $value = 0; $coupID = $id; $description = isset($infoW['description']) ? $infoW['description'] : ''; switch ($infoW["discountType"]) { case "Q": // quantity discount // discount = coupon's discountValue // times the cheapeast coupon item $valQ = "select unitPrice, department \n " . $this->baseSQL($transDB, $coupID, 'upc') . "\n and h.type in ('BOTH', 'DISCOUNT')\n and l.total > 0\n order by unitPrice asc "; $valR = $transDB->query($valQ); $valW = $transDB->fetch_row($valR); $value = $valW[0] * $infoW["discountValue"]; break; case "P": // discount price // query to get the item's department and current value // current value minus the discount price is how much to // take off $value = $infoW["discountValue"]; $deptQ = "select department, (total/quantity) as value \n " . $this->baseSQL($transDB, $coupID, 'upc') . "\n and h.type in ('BOTH', 'DISCOUNT')\n and l.total > 0\n order by unitPrice asc "; $deptR = $transDB->query($deptQ); $row = $transDB->fetch_row($deptR); $value = $row[1] - $value; break; case "FD": // flat discount for departments // simply take off the requested amount // scales with quantity for by-weight items $value = $infoW["discountValue"]; $valQ = "select department, quantity \n " . $this->baseSQL($transDB, $coupID, 'department') . "\n and h.type in ('BOTH', 'DISCOUNT')\n and l.total > 0\n order by unitPrice asc "; $valR = $transDB->query($valQ); $row = $transDB->fetch_row($valR); $value = $row[1] * $value; break; case "MD": // mix discount for departments // take off item value or discount value // whichever is less $value = $infoW["discountValue"]; $valQ = "select department, l.total \n " . $this->baseSQL($transDB, $coupID, 'department') . "\n and h.type in ('BOTH', 'DISCOUNT')\n and l.total > 0\n order by l.total desc "; $valR = $transDB->query($valQ); $row = $transDB->fetch_row($valR); $value = $row[1] < $value ? $row[1] : $value; break; case "AD": // all department discount // apply discount across all items // scales with quantity for by-weight items $value = $infoW["discountValue"]; $valQ = "select sum(quantity) \n " . $this->baseSQL($transDB, $coupID, 'department') . "\n and h.type in ('BOTH', 'DISCOUNT')\n and l.total > 0\n order by unitPrice asc "; $valR = $transDB->query($valQ); $row = $transDB->fetch_row($valR); $value = $row[1] * $value; break; case "FI": // flat discount for items // simply take off the requested amount // scales with quantity for by-weight items $value = $infoW["discountValue"]; $valQ = "select l.upc, quantity \n " . $this->baseSQL($transDB, $coupID, 'upc') . "\n and h.type in ('BOTH', 'DISCOUNT')\n and l.total > 0\n order by unitPrice asc"; $valR = $transDB->query($valQ); $row = $transDB->fetch_row($valR); $value = $row[1] * $value; break; case 'PI': // per-item discount // take of the request amount times the // number of matching items. $value = $infoW["discountValue"]; $valQ = "\n SELECT \n SUM(CASE WHEN ItemQtty IS NULL THEN 0 ELSE ItemQtty END) AS qty\n " . $this->baseSQL($transDB, $coupID, 'upc'); $valR = $transDB->query($valQ); $row = $transDB->fetch_row($valR); $value = $row['qty'] * $value; break; case "F": // completely flat; no scaling for weight $value = $infoW["discountValue"]; break; case "%": // percent discount on all items Database::getsubtotals(); $value = $infoW["discountValue"] * CoreLocal::get("discountableTotal"); break; case "%B": // better percent discount applies Database::getsubtotals(); $coupon_discount = (int) ($infoW['discountValue'] * 100); if ($coupon_discount <= CoreLocal::get('percentDiscount')) { // customer's discount is better than coupon discount; skip // applying coupon $value = 0; } else { // coupon discount is better than customer's discount // apply coupon & zero out customer's discount $value = $infoW["discountValue"] * CoreLocal::get("discountableTotal"); CoreLocal::set('percentDiscount', 0); $transDB->query('UPDATE localtemptrans SET percentDiscount=0'); } break; case "%D": // percent discount on all items in give department(s) $valQ = "select sum(total) \n " . $this->baseSQL($transDB, $coupID, 'department') . "\n and h.type in ('BOTH', 'DISCOUNT')"; $valR = $transDB->query($valQ); $row = $transDB->fetch_row($valR); $value = $row[0] * $infoW["discountValue"]; break; case "%E": // better percent discount applies to specified department only Database::getsubtotals(); $coupon_discount = (int) ($infoW['discountValue'] * 100); if ($coupon_discount <= CoreLocal::get('percentDiscount')) { // customer's discount is better than coupon discount; skip // applying coupon $value = 0; } else { // coupon discount is better than customer's discount // apply coupon & exclude those items from customer's discount $valQ = "select sum(total) \n " . $this->baseSQL($transDB, $coupID, 'department') . "\n and h.type in ('BOTH', 'DISCOUNT')"; $valR = $transDB->query($valQ); $row = $transDB->fetch_row($valR); $value = $row[0] * $infoW["discountValue"]; $clearQ = "\n UPDATE localtemptrans AS l \n INNER JOIN " . CoreLocal::get('pDatabase') . $transDB->sep() . "houseCouponItems AS h ON l.department = h.upc\n SET l.discountable=0\n WHERE h.coupID = " . $coupID . "\n AND h.type IN ('BOTH', 'DISCOUNT')"; $clearR = $transDB->query($clearR); } break; case 'PD': // modify customer percent discount // rather than add line-item $couponPD = $infoW['discountValue'] * 100; DiscountModule::updateDiscount(new DiscountModule($couponPD, 'HouseCoupon')); // still need to add a line-item with the coupon UPC to the // transaction to track usage $value = 0; $description = $couponPD . ' % Discount Coupon'; break; case 'OD': // override customer percent discount // rather than add line-item $couponPD = $infoW['discountValue'] * 100; DiscountModule::updateDiscount(new DiscountModule(0, 'custdata')); DiscountModule::updateDiscount(new DiscountModule($couponPD, 'HouseCoupon')); // still need to add a line-item with the coupon UPC to the // transaction to track usage $value = 0; $description = $couponPD . ' % Discount Coupon'; break; } return array('value' => $value, 'department' => $infoW['department'], 'description' => $description); }
public function testDiscountModules() { $ten = new DiscountModule(10, 'ten'); $fifteen = new DiscountModule(15, 'fifteen'); // verify stacking discounts CoreLocal::set('percentDiscount', 0); CoreLocal::set('NonStackingDiscounts', 0); DiscountModule::updateDiscount($ten, false); $this->assertEquals(10, CoreLocal::get('percentDiscount')); DiscountModule::updateDiscount($fifteen, false); $this->assertEquals(25, CoreLocal::get('percentDiscount')); DiscountModule::transReset(); // verify non-stacking discounts CoreLocal::set('percentDiscount', 0); CoreLocal::set('NonStackingDiscounts', 1); DiscountModule::updateDiscount($ten, false); $this->assertEquals(10, CoreLocal::get('percentDiscount')); DiscountModule::updateDiscount($fifteen, false); $this->assertEquals(15, CoreLocal::get('percentDiscount')); DiscountModule::transReset(); // verify best non-stacking discount wins CoreLocal::set('percentDiscount', 0); DiscountModule::updateDiscount($fifteen, false); $this->assertEquals(15, CoreLocal::get('percentDiscount')); DiscountModule::updateDiscount($ten, false); $this->assertEquals(15, CoreLocal::get('percentDiscount')); DiscountModule::transReset(); // verify same-name discounts overwrite $one = new DiscountModule(1, 'custdata'); $two = new DiscountModule(2, 'custdata'); CoreLocal::set('percentDiscount', 0); CoreLocal::set('NonStackingDiscounts', 0); DiscountModule::updateDiscount($one, false); $this->assertEquals(1, CoreLocal::get('percentDiscount')); DiscountModule::updateDiscount($two, false); $this->assertEquals(2, CoreLocal::get('percentDiscount')); DiscountModule::transReset(); // same-name should overwrite in the order called CoreLocal::set('percentDiscount', 0); DiscountModule::updateDiscount($two, false); $this->assertEquals(2, CoreLocal::get('percentDiscount')); DiscountModule::updateDiscount($one, false); $this->assertEquals(1, CoreLocal::get('percentDiscount')); }