fannie/item/modules/BaseItemModule.php
<?php
/*******************************************************************************
Copyright 2013 Whole Foods Co-op, Duluth, MN
This file is part of CORE-POS.
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
*********************************************************************************/
class BaseItemModule extends \COREPOS\Fannie\API\item\ItemModule
{
private function getBarcodeType($upc)
{
$trimmed = ltrim($upc, '0');
$barcode_type = '';
if (strlen($trimmed) == '12') {
// probably EAN-13 w/o check digi
$barcode_type = 'EAN';
} elseif (strlen($trimmed) == 11 && $trimmed[0] == '2') {
// variable price UPC
$barcode_type = 'Scale';
} elseif (strlen($trimmed) <= 11 && strlen($trimmed) >= 6) {
// probably UPC-A w/o check digit
$barcode_type = 'UPC';
} else {
$barcode_type = 'PLU';
}
return $barcode_type;
}
private function getStores()
{
$store_model = new StoresModel($this->db());
$store_model->hasOwnItems(1);
$stores = array();
foreach ($store_model->find('storeID') as $obj) {
$stores[$obj->storeID()] = $obj;
}
if (count($stores) == 0) {
$store_model->storeID(1);
$store_model->description('DEFAULT STORE');
$stores[1] = $store_model;
}
return $stores;
}
private function prevNextItem($upc, $department)
{
/* find previous and next items in department */
$dbc = $this->db();
$prevP = $dbc->prepare('SELECT upc FROM products WHERE department=? AND upc < ? ORDER BY upc DESC');
$nextP = $dbc->prepare('SELECT upc FROM products WHERE department=? AND upc > ? ORDER BY upc');
$prevUPC = $dbc->getValue($prevP, array($department, $upc));
$nextUPC = $dbc->getValue($nextP, array($department, $upc));
return array($prevUPC, $nextUPC);
}
private function getVendorName($vendorID)
{
$dbc = $this->db();
$prep = $dbc->prepare('SELECT vendorName FROM vendors WHERE vendorID=?');
$name = $dbc->getValue($prep, array($vendorID));
return ($name === false) ? '' : $name;
}
/**
Look for items with a similar UPC to guess what
department this item goes in. If found, use
department settings to fill in some defaults
*/
private function guessDepartment($upc)
{
$dbc = $this->db();
$search = substr($upc,0,12);
$searchP = $dbc->prepare('SELECT department FROM products WHERE upc LIKE ?');
$department = 0;
while (strlen($search) >= 8) {
$searchR = $dbc->execute($searchP,array($search.'%'));
if ($dbc->numRows($searchR) > 0) {
$searchW = $dbc->fetchRow($searchR);
$department = $searchW['department'];
break;
}
$search = substr($search,0,strlen($search)-1);
}
return $department;
}
private function confidentDepartment($upc)
{
$brand_id = substr($upc, 0, 8);
if ($brand_id === '00000000') {
return true;
}
$dbc = $this->db();
$chkP = $dbc->prepare('
SELECT department
FROM products
WHERE upc LIKE ?
AND upc not like \'002%\'
GROUP BY department
');
$chkR = $dbc->execute($chkP, array($brand_id . '%'));
return $dbc->numRows($chkR) === 1 ? true : false;
}
private function getExistingItem($upc)
{
$dbc = $this->db();
$itemQ = '
SELECT
p.description,
p.pricemethod,
p.normal_price,
p.cost,
CASE
WHEN p.size IS NULL OR p.size=\'\' OR p.size=\'0\' AND v.size IS NOT NULL THEN v.size
ELSE p.size
END AS size,
p.unitofmeasure,
p.modified,
p.last_sold,
p.special_price,
p.end_date,
p.subdept,
p.department,
p.tax,
p.foodstamp,
p.scale,
p.qttyEnforced,
p.discount,
p.line_item_discountable,
p.brand AS manufacturer,
n.vendorName AS distributor,
u.description as ldesc,
p.default_vendor_id,
v.units AS caseSize,
v.sku,
p.inUse,
p.idEnforced,
p.local,
p.deposit,
p.discounttype,
p.wicable,
p.store_id,
CASE WHEN c.upc IS NOT NULL THEN 1 ELSE 0 END AS inventoried,
c.count AS lastCount,
c.countDate,
c.par AS invPar,
i.onHand,
0 AS isAlias
FROM products AS p
LEFT JOIN productUser AS u ON p.upc=u.upc
LEFT JOIN vendorItems AS v ON p.upc=v.upc AND p.default_vendor_id = v.vendorID
LEFT JOIN vendors AS n ON p.default_vendor_id=n.vendorID
LEFT JOIN InventoryCache AS i ON p.upc=i.upc AND p.store_id=i.storeID
LEFT JOIN InventoryCounts AS c ON p.upc=c.upc AND p.store_id=c.storeID AND c.mostRecent=1
WHERE p.upc=?
ORDER BY v.modified DESC';
$p_def = $dbc->tableDefinition('products');
if (!isset($p_def['last_sold'])) {
$itemQ = str_replace('p.last_sold', 'NULL as last_sold', $itemQ);
}
$itemP = $dbc->prepare($itemQ);
$res = $dbc->execute($itemP,array($upc));
if ($dbc->numRows($res) > 0) {
$items = array();
while ($row = $dbc->fetchRow($res)) {
// make sure we only use the *first* record for each store.
// this is done specifically to ensure we have the "correct"
// (deterministic) pseudo-default vendorItems record, for those
// cases where it matters
if (!array_key_exists($row['store_id'], $items)) {
$items[$row['store_id']] = $row;
}
}
return $items;
}
return false;
}
private function getNewItem($upc)
{
$dbc = $this->db();
// default values for form fields
$rowItem = array(
'description' => '',
'normal_price' => 0,
'pricemethod' => 0,
'size' => '',
'unitofmeasure' => '',
'modified' => '',
'ldesc' => '',
'manufacturer' => '',
'distributor' => '',
'default_vendor_id' => 0,
'department' => 0,
'subdept' => 0,
'tax' => 0,
'foodstamp' => 0,
'scale' => 0,
'qttyEnforced' => 0,
'discount' => 1,
'line_item_discountable' => 1,
'caseSize' => '',
'sku' => '',
'inUse' => 1,
'idEnforced' => 0,
'local' => 0,
'deposit' => 0,
'cost' => 0,
'discounttype' => 0,
'wicable' => 0,
'inventoried' => 0,
'isAlias' => 0,
);
$ret = '';
if (class_exists('BrandAbbrFixModel')) {
$brandFix = new BrandAbbrFixModel($dbc);
$brandFixP = $dbc->prepare("SELECT goodName FROM BrandAbbrFix WHERE badName = ?");
}
/**
Check for entries in the vendorItems table to prepopulate
fields for the new item
*/
$vendorP = "
SELECT
i.description,
i.brand as manufacturer,
i.cost,
v.vendorName as distributor,
d.margin,
i.vendorID,
i.srp,
i.size,
i.units,
i.sku,
i.vendorID as default_vendor_id
FROM vendorItems AS i
LEFT JOIN vendors AS v ON i.vendorID=v.vendorID
LEFT JOIN vendorDepartments AS d ON i.vendorDept=d.deptID AND d.vendorID=i.vendorID
WHERE i.upc=?";
$args = array($upc);
$vID = FormLib::get('vid','');
if ($vID !== ''){
$vendorP .= ' AND i.vendorID=?';
$args[] = $vID;
}
$vendorP .= ' ORDER BY i.vendorID';
$vendorP = $dbc->prepare($vendorP);
$vendorR = $dbc->execute($vendorP,$args);
if ($dbc->numRows($vendorR) > 0){
$vrow = $dbc->fetchRow($vendorR);
$ret .= "<div><i>This product is in the ".$vrow['distributor']." catalog. Values have
been filled in where possible</i></div>";
$rowItem['description'] = $vrow['description'];
$rowItem['manufacturer'] = $vrow['manufacturer'];
$rowItem['cost'] = $vrow['cost'];
$rowItem['distributor'] = $vrow['distributor'];
$rowItem['normal_price'] = $vrow['srp'];
$rowItem['default_vendor_id'] = $vrow['vendorID'];
$rowItem['size'] = $vrow['size'];
$rowItem['caseSize'] = $vrow['units'];
$rowItem['sku'] = $vrow['sku'];
if (class_exists('BrandAbbrFixModel')) {
$brandFixR = $dbc->execute($brandFixP, array($rowItem['manufacturer']));
$brandFixW = $dbc->fetchRow($brandFixR);
if ($brandFixW) {
$fixedBrand = $brandFixW['goodName'];
if (strlen($fixedBrand) > 0) {
$rowItem['manufacturer'] = $fixedBrand;
}
}
}
while ($vrow = $dbc->fetchRow($vendorR)) {
$ret .= sprintf('This product is also in <a href="?searchupc=%s&vid=%d">%s</a><br />',
$upc,$vrow['vendorID'],$vrow['distributor']);
}
}
$rowItem['department'] = $this->guessDepartment($upc);
/**
If no match is found, pick the most
commonly used department
*/
if ($rowItem['department'] == 0) {
$commonP = $dbc->prepare('
SELECT department,
COUNT(*)
FROM products
GROUP BY department
ORDER BY COUNT(*) DESC');
$rowItem['department'] = $dbc->getValue($commonP);
}
/**
Get defaults for chosen department
*/
$dmodel = new DepartmentsModel($dbc);
$dmodel->dept_no($rowItem['department']);
if ($dmodel->load()) {
$rowItem['tax'] = $dmodel->dept_tax();
$rowItem['foodstamp'] = $dmodel->dept_fs();
$rowItem['discount'] = $dmodel->dept_discount();
$rowItem['line_item_discountable'] = $dmodel->line_item_discount();
$rowItem['wicable'] = $dmodel->dept_wicable();
}
return array($rowItem, $ret);
}
private function highlightUPC($upc)
{
switch ($this->getBarcodeType($upc)) {
case 'EAN':
case 'UPC':
return substr($upc, 0, 3)
. '<a class="text-danger iframe fancyboxLink" href="../reports/ProductLine/ProductLineReport.php?prefix='
. substr($upc, 3, 5) . '" title="Product Line">'
. '<strong>' . substr($upc, 3, 5) . '</strong>'
. '</a>'
. substr($upc, 8);
case 'Scale':
return substr($upc, 0, 3)
. '<strong>' . substr($upc, 3, 4) . '</strong>'
. substr($upc, 7);
case 'PLU':
$trimmed = ltrim($upc, '0');
if (strlen($trimmed) < 13) {
return str_repeat('0', 13-strlen($trimmed))
. '<strong>' . $trimmed . '</strong>';
}
}
return $upc;
}
public function showEditForm($upc, $display_mode=1, $expand_mode=1)
{
$FANNIE_PRODUCT_MODULES = FannieConfig::config('PRODUCT_MODULES', array());
$upc = BarcodeLib::padUPC($upc);
$ret = '<div id="BaseItemFieldset" class="panel panel-default">';
$dbc = $this->db();
$stores = $this->getStores();
$items = array();
$rowItem = array();
$prevUPC = false;
$nextUPC = false;
$likeCode = false;
$exists = $this->getExistingItem($upc);
if ($exists) {
$items = $exists;
$rowItem = current($items);
$new_item = false;
$rowItem['distributor'] = $this->getVendorName($rowItem['default_vendor_id']);
/* find previous and next items in department */
list($prevUPC, $nextUPC) = $this->prevNextItem($upc, $rowItem['department']);
$lcP = $dbc->prepare('SELECT likeCode FROM upcLike WHERE upc=?');
$likeCode = $dbc->getValue($lcP,array($upc));
if (FannieConfig::config('STORE_MODE') == 'HQ') {
$default_id = array_keys($items);
$default_id = $default_id[0];
$default_item = $items[$default_id];
foreach ($stores as $id => $info) {
if (!isset($items[$id])) {
$items[$id] = $default_item;
}
}
}
} else {
list($rowItem, $msg) = $this->getNewItem($upc);
$ret .= $msg;
$new_item = true;
foreach ($stores as $id => $obj) {
$items[$id] = $rowItem;
}
}
$ret .= '<div class="panel-heading">';
if ($prevUPC) {
$ret .= ' <a class="btn btn-default btn-xs small" href="ItemEditorPage.php?searchupc=' . $prevUPC . '"
title="Previous item in this department">
<span class="fas fa-chevron-left"></span></a> ';
}
$ret .= '<strong>UPC</strong>
<span class="text-danger">';
$ret .= $this->highlightUPC($upc);
$ret .= '</span>';
$ret .= '<input type="hidden" id="upc" name="upc" value="' . $upc . '" />';
if ($nextUPC) {
$ret .= ' <a class="btn btn-default btn-xs small" href="ItemEditorPage.php?searchupc=' . $nextUPC . '"
title="Next item in this department">
<span class="fas fa-chevron-right"></span></a>';
}
$ret .= ' <label style="color:darkmagenta;">Modified</label>
<span style="color:darkmagenta;">'. $rowItem['modified'] . '</span>';
$ret .= ' | <label style="color:darkmagenta;">Last Sold</label> ';
$netStore = COREPOS\Fannie\API\lib\Store::getIdByIp();
foreach ($items as $store_id => $item) {
$ret .= sprintf('<span style="color:darkmagenta;" class="last-sold %s" id="last-sold%d">%s</span>',
($store_id == $netStore ? '' : 'collapse'),
$store_id,
(!empty($item['last_sold']) ? $item['last_sold'] : 'n/a')
);
}
if (FannieConfig::config('COOP_ID') == 'WFC_Duluth') {
$ret .= ' | <a href="mapping/FindItem.php?id=' . $upc . '">Find it!</a>';
}
$ret .= '</div>'; // end panel-heading
$ret .= '<div class="panel-body">';
if ($new_item) {
// new item
$ret .= "<div class=\"alert alert-warning\">Item not found. You are creating a new one.</div>";
if (strlen($rowItem['sku']) > 0) {
$ret .= "<div class=\"well\">Click <a onclick=\"baseItem.deleteVendorItem('{$rowItem['sku']}', {$rowItem['default_vendor_id']}); return false; \" style=\"color: brown; cursor: pointer;\">here </a> to delete this information and free SKU for use elsewhere (and begin with clean slate).</div>";
}
if (!$this->confidentDepartment($upc)) {
$ret .= '<div class="alert alert-danger">Please double-check POS department auto selection</div>';
}
}
$nav_tabs = '<ul id="store-tabs" class="nav nav-tabs small" role="tablist">';
$ret .= '{{nav_tabs}}<div class="tab-content">';
foreach ($items as $store_id => $rowItem) {
$active_tab = false;
if (FannieConfig::config('STORE_MODE') !== 'HQ' || $netStore == $store_id || ($netStore == false && $store_id == FannieConfig::config('STORE_ID'))) {
$active_tab = true;
}
$tabID = 'store-tab-' . $store_id;
$store_description = 'n/a';
if (isset($stores[$store_id])) {
$store_description = $stores[$store_id]->description();
}
$nav_tabs .= '<li role="presentation" ' . ($active_tab ? 'class="active"' : '') . '>'
. '<a href="#' . $tabID . '" aria-controls="' . $tabID . '" '
. 'onclick="$(\'.tab-content .chosen-select:visible\').chosen();
$(\'.last-sold\').hide(); $(\'#last-sold' . $store_id . '\').show();"'
. 'role="tab" data-toggle="tab">'
. $store_description . '</a></li>';
$ret .= '<div role="tabpanel" class="tab-pane' . ($active_tab ? ' active' : '') . '"
id="' . $tabID . '">';
$ret .= '<input type="hidden" class="store-id" name="store_id[]" value="' . $store_id . '" />';
$ret .= '<div id="" class=""><table id="productTable'.$store_id.'" class="table table-bordered">';
$jsVendorID = $rowItem['default_vendor_id'] != 0 ? $rowItem['default_vendor_id'] : 'no-vendor';
$vFieldsDisabled = $jsVendorID == 'no-vendor' || !$active_tab ? 'disabled' : '';
$aliasDisabled = $rowItem['isAlias'] ? 'disabled' : '';
$limit = 30 - strlen(isset($rowItem['description'])?$rowItem['description']:'');
$cost = sprintf('%.3f', $rowItem['cost']);
$price = sprintf('%.2f', $rowItem['normal_price']);
$ret .= <<<HTML
<tr>
<th class="text-right">Description</th>
<td colspan="5">
<div class="input-group" style="width:100%;">
<input type="text" maxlength="30" class="form-control syncable-input descript-input" required
name="descript[]" value="{$rowItem['description']}"
style="text-transform:uppercase;"
onkeyup="$(this).next().html(30-(this.value.length));" />
<span class="input-group-addon">{$limit}</span>
</div>
</td>
<th class="text-right">Cost</th>
<td>
<div class="input-group">
<span class="input-group-addon">$</span>
<input type="text" id="cost{$store_id}" name="cost[]"
class="form-control price-field cost-input syncable-input"
value="{$cost}" data-store-id="{$store_id}" maxlength="6"
onkeydown="if (typeof nosubmit == 'function') nosubmit(event);"
onkeyup="if (typeof nosubmit == 'function') nosubmit(event);"
onchange="$('.default_vendor_cost').val(this.value);"
/>
</div>
</td>
<th class="text-right">Price</th>
<td>
<div class="input-group">
<span class="input-group-addon">$</span>
<input type="text" id="price{$store_id}" name="price[]"
class="form-control price-field price-input syncable-input"
data-store-id="{$store_id}" maxlength="6"
required value="{$price}" />
</div>
</td>
</tr>
HTML;
// no need to display this field twice
if (!isset($FANNIE_PRODUCT_MODULES['ProdUserModule'])) {
$ret .= '
<tr>
<th>Long Desc.</th>
<td colspan="5">
<input type="text" size="60" name="puser_description" maxlength="255"
' . (!$active_tab ? ' disabled ' : '') . '
value="' . $rowItem['ldesc'] . '" class="form-control" />
</td>
</tr>';
}
$ret .= '
<tr>
<th class="text-right">Brand</th>
<td colspan="5">
<input type="text" name="manufacturer[]"
class="form-control input-sm brand-field syncable-input"
style="text-transform:uppercase;"
value="' . $rowItem['manufacturer'] . '" />
</td>';
/**
Check products.default_vendor_id to see if it is a
valid reference to the vendors table
*/
$normalizedVendorID = false;
if (isset($rowItem['default_vendor_id']) && $rowItem['default_vendor_id'] <> 0) {
$normalizedVendorID = $rowItem['default_vendor_id'];
}
$ret .= ' <th class="text-right">Vendor</th> ';
$ret .= '<td colspan="3" class="form-inline"><select name="distributor[]"
class="chosen-select form-control vendor_field syncable-input"
onchange="baseItem.vendorChanged(this.value);">';
$ret .= '<option value="0">Select a vendor</option>';
$vendR = $dbc->query('SELECT vendorID, vendorName FROM vendors WHERE inactive=0 ORDER BY vendorName');
while ($vendW = $dbc->fetchRow($vendR)) {
$ret .= sprintf('<option %s>%s</option>',
($vendW['vendorID'] == $rowItem['default_vendor_id'] ? 'selected' : ''),
$vendW['vendorName']);
}
$ret .= '</select>';
$ret .= ' <button type="button"
title="Create new vendor"
class="btn btn-default btn-sm newVendorButton">
<span class="fas fa-plus"></span></button>';
$ret .= '</td></tr>'; // end row
if (isset($rowItem['discounttype']) && $rowItem['discounttype'] <> 0) {
/* show sale info */
if (FannieConfig::config('STORE_MODE') == 'HQ') {
$batchP = $dbc->prepare("
SELECT b.batchName,
b.batchID
FROM batches AS b
LEFT JOIN batchList as l on b.batchID=l.batchID
LEFT JOIN StoreBatchMap AS m ON b.batchID=m.batchID
WHERE '" . date('Y-m-d') . "' BETWEEN b.startDate AND b.endDate
AND (l.upc=? OR l.upc=?)
AND m.storeID=?
AND b.discountType > 0"
);
$batchR = $dbc->execute($batchP,array($upc,'LC'.$likeCode,$store_id));
} else {
$batchP = $dbc->prepare("
SELECT b.batchName,
b.batchID
FROM batches AS b
LEFT JOIN batchList as l on b.batchID=l.batchID
WHERE '" . date('Y-m-d') . "' BETWEEN b.startDate AND b.endDate
AND (l.upc=? OR l.upc=?)
AND b.discountType > 0
");
$batchR = $dbc->execute($batchP,array($upc,'LC'.$likeCode));
}
$batch = array('batchID'=>0, 'batchName'=>"Unknown");
if ($dbc->num_rows($batchR) > 0) {
$batch = $dbc->fetch_row($batchR);
}
$ret .= '<td class="alert-success" colspan="8">';
$ret .= sprintf("<strong>Sale Price:</strong>
%.2f (<em>Batch: <a href=\"%sbatches/newbatch/EditBatchPage.php?id=%d\">%s</a></em>)",
$rowItem['special_price'], FannieConfig::config('URL'), $batch['batchID'], $batch['batchName']);
list($date,$time) = explode(' ',$rowItem['end_date']);
$ret .= "<strong>End Date:</strong>
$date
(<a href=\"EndItemSale.php?id=$upc\">Unsale Now</a>)";
$ret .= '</td>';
}
$supers = array();
$depts = array();
$subs = array();
$superID = '';
$range_limit = FannieAuth::validateUserLimited('pricechange');
list($superID, $supers, $depts, $subs) = $this->deptMaps($rowItem, $range_limit);
$names = new SuperDeptNamesModel($dbc);
$superQ = 'SELECT superID, super_name FROM superDeptNames';
$superArgs = array();
if (is_array($range_limit) && count($range_limit) == 2) {
$superArgs = $range_limit;
$superQ .= ' WHERE superID BETWEEN ? AND ? ';
}
$superQ .= ' ORDER BY superID';
$superP = $dbc->prepare($superQ);
$superR = $dbc->execute($superP, $superArgs);
$superOpts = '';
while ($superW = $dbc->fetchRow($superR)) {
$superOpts .= sprintf('<option %s value="%d">%s</option>',
$superW['superID'] == $superID ? 'selected' : '',
$superW['superID'], $superW['super_name']);
}
$deptOpts = '';
foreach ($depts as $id => $name){
if (is_numeric($superID) && is_array($supers[$superID])) {
if (!in_array($id, $supers[$superID]) && $id != $rowItem['department']) {
continue;
}
}
$deptOpts .= sprintf('<option %s value="%d">%d %s</option>',
($id == $rowItem['department'] ? 'selected':''),
$id,$id,$name);
}
$codingP = $dbc->prepare("SELECT salesCode FROM departments WHERE dept_no=?");
$coding = $dbc->getValue($codingP, array($rowItem['department']));
$subOpts = isset($subs[$rowItem['department']]) ? $subs[$rowItem['department']] : '';
// subdept zero is selected
$subZero = $rowItem['subdept'] == 0 ? 'selected' : '';
$taxQ = $dbc->prepare('SELECT id,description FROM taxrates ORDER BY id');
$taxR = $dbc->execute($taxQ);
$rates = array();
while ($taxW = $dbc->fetch_row($taxR)) {
array_push($rates,array($taxW[0],$taxW[1]));
}
array_push($rates,array("0","NoTax"));
$rateOpts = '';
foreach ($rates as $r) {
$rateOpts .= sprintf('<option %s value="%d">%s</option>',
(isset($rowItem['tax'])&&$rowItem['tax']==$r[0]?'selected':''),
$r[0],$r[1]);
}
$fsCheck = $rowItem['foodstamp'] == 1 ? 'checked' : '';
$scaleCheck = $rowItem['scale'] == 1 ? 'checked' : '';
$qtyCheck = $rowItem['qttyEnforced'] == 1 ? 'checked' : '';
$wicCheck = $rowItem['wicable'] == 1 ? 'checked' : '';
$inUseCheck = $rowItem['inUse'] == 1 ? 'checked' : '';
$disc_opts = array(
0 => 'No',
1 => 'Yes',
2 => 'Trxn Only',
3 => 'Line Only',
);
$rowItem['discount'] = $this->mapDiscounts($rowItem['discount'], $rowItem['line_item_discountable']);
$discountOpts = '';
foreach ($disc_opts as $id => $val) {
$discountOpts .= sprintf('<option %s value="%d">%s</option>',
($id == $rowItem['discount'] ? 'selected' : ''),
$id, $val);
}
$deposit = ($rowItem['deposit'] != 0) ? $rowItem['deposit'] : '';
$ageOpts = '';
foreach (array('n/a'=>0, 18=>18, 21=>21) as $label=>$age) {
$ageOpts .= sprintf('<option %s value="%d">%s</option>',
($age == $rowItem['idEnforced'] ? 'selected' : ''),
$age, $label);
}
$local_opts = array(0=>'No');
$origin = new OriginsModel($dbc);
$local_opts = array_merge($local_opts, $origin->getLocalOrigins());
if (count($local_opts) == 1) {
$local_opts[1] = 'Yes'; // generic local if no origins defined
}
$localOpts = '';
foreach($local_opts as $id => $val) {
$localOpts .= sprintf('<option value="%d" %s>%s</option>',
$id, ($id == $rowItem['local']?'selected':''), $val);
}
$ret .= <<<HTML
<tr>
<th class="text-right">Dept</th>
<td colspan="7" class="form-inline">
<select id="super-dept{$store_id}" name="super[]"
class="form-control chosen-select syncable-input"
onchange="chainSuperDepartment('../ws/', this.value, {
dept_start:'#department{$store_id}',
callback:function(){
\$('#department{$store_id}').trigger('chosen:updated');
if ($('#store-sync').prop('checked') === false) {
return;
}
baseItem.chainSubs({$store_id});
var opts = $('#department{$store_id}').html();
$('.chosen-dept').each(function(i, e) {
if (e.id != 'department{$store_id}') {
$(e).html(opts);
$(e).trigger('chosen:updated');
baseItem.chainSubs(e.id.substring(10));
}
});
}
});">
{$superOpts}
</select>
<select name="department[]" id="department{$store_id}"
class="form-control chosen-select chosen-dept syncable-input"
onchange="baseItem.chainSubs({$store_id});">
{$deptOpts}
</select>
<select name="subdept[]" id="subdept{$store_id}"
class="form-control chosen-select chosen-subdept syncable-input">
<option {$subZero} value="0">None</option>
{$subOpts}
</select>
{$coding}
</td>
<th class="small text-right sku-label" ondblclick="baseItem.toggleSkuOverride();">SKU</th>
<td colspan="2">
<input type="text" name="vendorSKU" value="{$rowItem['sku']}"
class="form-control input-sm sku-field syncable-input"
onchange="$('#vsku{$jsVendorID}').val(this.value);"
{$vFieldsDisabled} {$aliasDisabled} />
<input type="hidden" name="isAlias" value="{$rowItem['isAlias']}" />
<input type="hidden" name="origSKU" value="{$rowItem['sku']}" {$vFieldsDisabled} />
</td>
</tr>
<tr>
<th class="small text-right">Tax</th>
<td>
<select name="tax[]" id="tax{$store_id}"
class="form-control input-sm syncable-input">
{$rateOpts}
</select></td>
<td colspan="4" class="small">
<label>FS
<input type="checkbox" value="{$store_id}" name="FS[]" id="FS{$store_id}"
class="syncable-checkbox" {$fsCheck} />
</label>
<label>Scale
<input type="checkbox" value="{$store_id}" name="Scale[]"
class="scale-checkbox syncable-checkbox" {$scaleCheck} />
</label>
<label>QtyFrc
<input type="checkbox" value="{$store_id}" name="QtyFrc[]"
class="qty-checkbox syncable-checkbox" {$qtyCheck} />
</label>
<label>WIC
<input type="checkbox" value="{$store_id}" id="wic{$store_id}" name="prod-wicable[]"
class="prod-wicable-checkbox syncable-checkbox" {$wicCheck} />
</label>
<label>InUse
<input type="checkbox" value="{$store_id}" name="prod-in-use[]"
class="in-use-checkbox" {$inUseCheck}
onchange="$('#extra-in-use-checkbox').prop('checked', $(this).prop('checked'));" />
</label>
</td>
<th class="small text-right">Discount</th>
<td class="col-sm-1">
<select id="discount-select{$store_id}" name="discount[]"
class="form-control input-sm syncable-input">
{$discountOpts}
</select>
</td>
<th class="small text-right">Deposit</th>
<td colspan="2">
<input type="text" name="deposit-upc[]" class="form-control input-sm syncable-input"
value="{$deposit}" placeholder="Deposit Item PLU/UPC"
onchange="\$('#deposit').val(this.value);" />
</td>
</tr>
<tr>
<th class="small text-right">Case Size</th>
<td class="col-sm-1">
<input type="text" name="caseSize"
class="form-control input-sm product-case-size"
value="{$rowItem['caseSize']}"
onchange="\$('#vunits{$jsVendorID}').val(this.value);"
{$vFieldsDisabled} {$aliasDisabled} />
</td>
<th class="small text-right">Pack Size</th>
<td class="col-sm-1">
<input type="text" name="size[]"
class="form-control input-sm product-pack-size syncable-input"
value="{$rowItem['size']}"
onchange="\$('#vsize{$jsVendorID}').val(this.value);" />
</td>
<th class="small text-right">Unit of measure</th>
<td class="col-sm-1">
<input type="text" name="unitm[]"
class="form-control input-sm unit-of-measure syncable-input"
value="{$rowItem['unitofmeasure']}" />
</td>
<th class="small text-right">Age Req</th>
<td class="col-sm-1">
<select name="id-enforced[]" class="form-control input-sm id-enforced syncable-input"
onchange="\$('#idReq').val(this.value);">
{$ageOpts}
</select>
</td>
<th class="small text-right">Local</th>
<td>
<select name="prod-local[]" class="form-control input-sm prod-local syncable-input"
onchange="\$('#local-origin-id').val(this.value);">
{$localOpts}
</select>
</td>
</tr>
HTML;
if ($rowItem['inventoried']) {
$ret .= sprintf('<tr>
<th class="small text-right">On Hand</th><td class="small">%d</td>
<th class="small text-right">Last Count</th><td colspan="2" class="small">%d on %s</td>
<th class="small text-right">Par</th><td class="small">%s</td>
<td colspan="3" class="small"><a href="inventory/InvCountPage.php?id=%s&store=%d">Adjust count/par</a></td>
</tr>',
$rowItem['onHand'],
$rowItem['lastCount'], $rowItem['countDate'],
$rowItem['invPar'],
$upc,
$store_id
);
}
$ret .= $this->getRowMods($upc, $active_tab, $store_id);
$ret .= '</table></div></div>';
if (FannieConfig::config('STORE_MODE') != 'HQ') {
break;
}
}
$ret .= '</div>';
// sync button will copy current tab values to all other store tabs
if (!$new_item && FannieConfig::config('STORE_MODE') == 'HQ') {
$nav_tabs .= '<li><label title="Apply update to all stores">
<input type="checkbox" id="store-sync" ';
//$audited = FannieAuth::validateUserQuiet('audited_pricechange');
//$nav_tabs .= ($audited) ? 'disabled' : 'checked';
$nav_tabs .= 'checked';
$nav_tabs .= ' /> Sync</label></li>';
}
$nav_tabs .= '</ul>';
// only show the store tabs in HQ mode
if (FannieConfig::config('STORE_MODE') == 'HQ') {
$ret = str_replace('{{nav_tabs}}', $nav_tabs, $ret);
} else {
$ret = str_replace('{{nav_tabs}}', '', $ret);
}
$ret .= <<<HTML
<div id="newVendorDialog" title="Create new Vendor" class="collapse">
<fieldset>
<label for="newVendorName">Vendor Name</label>
<input type="text" name="newVendorName" id="newVendorName" class="form-control" />
</fieldset>
</div>
<input type="hidden" value="0" id="skuOverride" name="skuOverride" />
</div> <!-- end panel-body -->
</div> <!-- end panel-->
HTML;
return $ret;
}
private function getRowMods($upc, $active_tab, $store_id)
{
$mods = FannieConfig::config('PRODUCT_ROWS');
asort($mods);
$ret = '';
foreach (array_keys($mods) as $mod) {
if (!class_exists($mod, false)) {
include(__DIR__ . '/' . $mod . '.php');
}
$obj = new $mod();
$ret .= $obj->formRow($upc, $active_tab, $store_id);
}
return $ret;
}
private function saveRowMods($upc)
{
$mods = FannieConfig::config('PRODUCT_ROWS');
asort($mods);
foreach (array_keys($mods) as $mod) {
if (!class_exists($mod, false)) {
include(__DIR__ . '/' . $mod . '.php');
}
$obj = new $mod();
$obj->setConfig($this->config);
$obj->setForm($this->form);
$obj->setConnection($this->connection);
$obj->saveFormData($upc);
}
}
public function getFormJavascript($upc)
{
return file_get_contents(__DIR__ . '/baseItem.js');
}
private function formNoEx($field, $default)
{
try {
return $this->form->{$field};
} catch (Exception $ex) {
return $default;
}
}
function saveFormData($upc)
{
$FANNIE_PRODUCT_MODULES = FannieConfig::config('PRODUCT_MODULES', array());
$upc = BarcodeLib::padUPC($upc);
$dbc = $this->db();
$model = new ProductsModel($dbc);
$model->upc($upc);
$stores = $this->formNoEx('store_id', array());
for ($i=0; $i<count($stores); $i++) {
$model->store_id($stores[$i]);
if (!$model->load()) {
// fully init new record
$model->pricemethod(0);
$model->groupprice(0.00);
$model->quantity(0);
$model->special_price(0);
$model->specialpricemethod(0);
$model->specialquantity(0);
$model->specialgroupprice(0);
$model->advertised(0);
$model->tareweight(0);
$model->start_date('1900-01-01');
$model->end_date('1900-01-01');
$model->discounttype(0);
$model->wicable(0);
$model->scaleprice(0);
$model->inUse(1);
$model->created(date('Y-m-d H:i:s'));
$model->last_sold(null);
}
$taxes = $this->formNoEx('tax', array());
if (isset($taxes[$i])) {
$model->tax($taxes[$i]);
}
$fs = $this->formNoEx('FS', array());
$model->foodstamp(in_array($stores[$i], $fs) ? 1 : 0);
$scale = $this->formNoEx('Scale', array());
$model->scale(in_array($stores[$i], $scale) ? 1 : 0);
$qtyFrc = $this->formNoEx('QtyFrc', array());
$model->qttyEnforced(in_array($stores[$i], $qtyFrc) ? 1 : 0);
$wic = FormLib::get('prod-wicable', array());
$model->wicable(in_array($stores[$i], $wic) ? 1 : 0);
$discount_setting = $this->formNoEx('discount', array());
if (isset($discount_setting[$i])) {
switch ($discount_setting[$i]) {
case 0:
$model->discount(0);
$model->line_item_discountable(0);
break;
case 1:
$model->discount(1);
$model->line_item_discountable(1);
break;
case 2:
$model->discount(1);
$model->line_item_discountable(0);
break;
case 3:
$model->discount(0);
$model->line_item_discountable(1);
break;
}
}
$price = $this->formNoEx('price', array());
if (isset($price[$i])) {
$model->normal_price($price[$i]);
}
$cost = $this->formNoEx('cost', array());
if (isset($cost[$i]) && $cost[$i] != '') {
$model->cost($cost[$i]);
}
$desc = $this->formNoEx('descript', array());
if (isset($desc[$i])) {
$desc[$i] = substr($desc[$i],0,30);
$model->description(strtoupper($desc[$i]));
}
$brand = $this->formNoEx('manufacturer', array());
if (isset($brand[$i])) {
$brand[$i] = substr($brand[$i],0,30);
$model->brand(strtoupper($brand[$i]));
}
/**
$model->pricemethod(0);
$model->groupprice(0.00);
$model->quantity(0);
*/
$dept = $this->formNoEx('department', array());
if (isset($dept[$i])) {
$model->department($dept[$i]);
}
$size = $this->formNoEx('size', array());
if (isset($size[$i])) {
$model->size($size[$i]);
}
$model->modified(date('Y-m-d H:i:s'));
$unit = FormLib::get('unitm');
$unit = $this->formNoEx('unitm', array());
if (isset($unit[$i])) {
$model->unitofmeasure($unit[$i]);
}
$subdept = $this->formNoEx('subdept', array());
if (isset($subdept[$i])) {
$model->subdept($subdept[$i]);
}
// lookup vendorID by name
$vendorID = 0;
$v_input = $this->formNoEx('distributor', array());
if (isset($v_input[$i])) {
$vendorID = $this->getVendorID($v_input[$i]);
}
$model->default_vendor_id($vendorID);
$inUse = FormLib::get('prod-in-use', array());
$model->inUse(in_array($stores[$i], $inUse) ? 1 : 0);
$idEnf = FormLib::get('id-enforced', array());
if (isset($idEnf[$i])) {
$model->idEnforced($idEnf[$i]);
}
$local = FormLib::get('prod-local');
if (isset($local[$i])) {
$model->local($local[$i]);
}
$deposit = FormLib::get('deposit-upc');
if (isset($deposit[$i])) {
if ($deposit[$i] == '') {
$deposit[$i] = 0;
}
$model->deposit($deposit[$i]);
}
$model->formatted_name($this->formatName($i));
$model->save();
}
/**
If a vendor is selected, intialize
a vendorItems record
*/
if ($vendorID != 0) {
$this->saveVendorItem($model, $vendorID);
}
if ($dbc->tableExists('prodExtra')) {
$this->saveProdExtra($model);
}
if (!isset($FANNIE_PRODUCT_MODULES['ProdUserModule']) && $dbc->tableExists('productUser')) {
$this->saveProdUser($upc);
}
$this->saveRowMods($upc);
}
private function getVendorID($name)
{
$dbc = $this->db();
$vendor = new VendorsModel($dbc);
$vendor->vendorName($name);
foreach ($vendor->find('vendorID') as $obj) {
return $obj->vendorID();
}
return 0;
}
private function formatName($index)
{
/* products.formatted_name is intended to be maintained automatically.
* Get all enabled plugins and standard modules of the base.
* Run plugins first, then standard modules.
*/
$formatters = FannieAPI::ListModules('ProductNameFormatter');
$fmt_name = "";
$fn_params = array('index' => $index);
foreach ($formatters as $formatter_name) {
$formatter = new $formatter_name();
$fmt_name = $formatter->compose($fn_params);
if (isset($formatter->this_mod_only) &&
$formatter->this_mod_only) {
break;
}
}
return $fmt_name;
}
private function saveProdUser($upc)
{
try {
$dbc = $this->db();
$model = new ProductUserModel($dbc);
$model->upc($upc);
$model->description($this->form->puser_description);
return $model->save();
} catch (Exception $ex) {
return false;
}
}
private function saveProdExtra($product)
{
$dbc = $this->db();
$extra = new ProdExtraModel($dbc);
$extra->upc($product->upc());
if (!$extra->load()) {
$extra->variable_pricing(0);
$extra->margin(0);
$extra->case_quantity('');
$extra->case_cost(0.00);
$extra->case_info('');
}
$extra->manufacturer($product->brand());
$extra->cost($product->cost());
try {
$extra->distributor($this->form->distributor[0]);
} catch (Exception $ex) {
$extra->distributor('');
}
return $extra->save();
}
private function saveVendorItem($product, $vendorID)
{
$dbc = $this->db();
$upc = $product->upc();
/**
If a vendor is selected, intialize
a vendorItems record
*/
$vitem = new VendorItemsModel($dbc);
$vitem->vendorID($vendorID);
$vitem->upc($upc);
try {
$sku = trim($this->form->vendorSKU);
$caseSize = $this->form->caseSize;
$alias = $this->form->isAlias;
$override = $this->form->skuOverride;
if ($alias) {
return true;
}
/**
* Always require a sku. use upc if none available
*/
if ($sku == '') {
$sku = $upc;
}
/**
* Must have a valid value for the data type.
* The default in the Model is 1.
*/
if (empty($caseSize)) {
$caseSize = 1;
}
$chkP = $dbc->prepare("SELECT upc FROM vendorItems
WHERE sku=? AND vendorID=? AND upc <> ?");
$chk = $dbc->getValue($chkP, array($sku, $vendorID, $upc));
if ($chk && $override == 0) {
// bail out. same sku cannot be assigned to multiple items
return true;
}
$vrecords = $vitem->find();
if (count($vrecords) > 1) {
// bail out. multiple matching records will cause ambiguity
return true;
}
if (count($vrecords) > 0) {
$editP = $dbc->prepare('
UPDATE vendorItems
SET sku=?
WHERE upc=?
AND vendorID=?
');
$editR = $dbc->execute($editP, array($sku, $upc, $vendorID));
}
} catch (Exception $ex) {
$sku = $upc;
$caseSize = 1;
}
$vitem->sku($sku);
$vitem->size($product->size());
$vitem->description($product->description());
$vitem->brand($product->brand());
$vitem->units($caseSize);
$vitem->cost($product->cost());
return $vitem->save();
}
function summaryRows($upc)
{
$dbc = $this->db();
$model = new ProductsModel($dbc);
$model->upc($upc);
if ($model->load()) {
$row1 = '<th>UPC</th>
<td><a href="ItemEditorPage.php?searchupc=' . $upc . '">' . $upc . '</td>
<td>
<a class="iframe fancyboxLink" href="addShelfTag.php?upc='.$upc.'" title="Create Shelf Tag">Shelf Tag</a>
</td>';
$row2 = '<th>Description</th><td>' . $model->description() . '</td>
<th>Price</th><td>$' . $model->normal_price() . '</td>';
$ret = array($row1, $row2);
$vendorID = $model->default_vendor_id();
$sku = FormLib::get('vendorSKU');
if ($vendorID != 0 && $sku != '') {
$chkP = $dbc->prepare("SELECT upc FROM vendorItems
WHERE sku=? AND vendorID=? AND upc <> ?");
$chk = $dbc->getValue($chkP, array($sku, $vendorID, $upc));
if ($chk) {
$ret[] = '<th class="danger">Error</th>
<td class="danger" colspan="3">SKU already assigned to <a href="ItemEditorPage.php?searchupc=' . $chk . '">' . $chk . '</a>.
Other changes saved.</td>';
}
if ($sku != FormLib::get('origSKU')) {
$multiP = $dbc->prepare("SELECT upc FROM vendorItems WHERE upc=? AND vendorID=?");
$multiR = $dbc->execute($multiP, array($upc, $vendorID));
if ($dbc->numRows($multiR) > 1) {
$ret[] = '<th class="danger">Error</th>
<td class="danger" colspan="3">Could not save SKU due to too many catalog entries.
Other changes saved.</td>';
}
}
}
return $ret;
} else {
return array('<td colspan="4">Error saving. <a href="ItemEditorPage.php?searchupc=' . $upc . '">Try Again</a>?</td>');
}
}
private function mapDiscounts($disc, $line)
{
if ($disc == 1 && $line == 1) {
return 1;
} elseif ($disc == 1 && $line == 0) {
return 2;
} elseif ($disc == 0 && $line == 1) {
return 3;
}
return $disc;
}
/**
Build ID=>name mappings for department tiers
$supers lists its departments
$subs is indexed by parent department number
*/
private function deptMaps($rowItem, $range_limit)
{
$supers = array();
$depts = array();
$subs = array();
$superID = '';
$dbc = $this->db();
$dDef = $dbc->tableDefinition('departments');
$active = isset($dDef['active']) ? ' d.active=1 ' : '';
$deptQ = '
SELECT dept_no,
dept_name,
subdept_no,
subdept_name,
s.dept_ID,
MIN(m.superID) AS superID
FROM departments AS d
LEFT JOIN subdepts AS s ON d.dept_no=s.dept_ID
LEFT JOIN superdepts AS m ON d.dept_no=m.dept_ID ';
if (is_array($range_limit) && count($range_limit) == 2) {
$deptQ .= ' WHERE m.superID BETWEEN ? AND ? AND ' . $active;
} else {
$range_limit = array();
$deptQ .= ' WHERE ' . $active;
}
$deptQ .= '
GROUP BY d.dept_no,
d.dept_name,
s.subdept_no,
s.subdept_name,
s.dept_ID
ORDER BY d.dept_no, s.subdept_name';
$deptP = $dbc->prepare($deptQ);
$deptR = $dbc->execute($deptP, $range_limit);
while ($row = $dbc->fetchRow($deptR)) {
if (!isset($depts[$row['dept_no']])) $depts[$row['dept_no']] = $row['dept_name'];
if ($row['dept_no'] == $rowItem['department']) {
$superID = $row['superID'];
}
if (!isset($supers[$row['superID']])) {
$supers[$row['superID']] = array();
}
$supers[$row['superID']][] = $row['dept_no'];
if ($row['subdept_no'] == '') {
continue;
}
if (!isset($subs[$row['dept_ID']]))
$subs[$row['dept_ID']] = '';
$subs[$row['dept_ID']] .= sprintf('<option %s value="%d">%d %s</option>',
($row['subdept_no'] == $rowItem['subdept'] ? 'selected':''),
$row['subdept_no'],$row['subdept_no'],$row['subdept_name']);
}
return array($superID, $supers, $depts, $subs);
}
}