diff --git a/action.php b/action.php index 7619cf0..2e6f7fc 100644 --- a/action.php +++ b/action.php @@ -363,4 +363,64 @@ switch ($VARS['action']) { session_destroy(); header('Location: index.php?logout=1'); die("Logged out."); + case "addstock": + $insert = true; + + if (empty($VARS['stock'])) { + $VARS['stock'] = 1; + } else if (!is_numeric($VARS['stock'])) { + returnToSender('field_nan'); + } + + $user = $_SESSION['uid']; + + $data = [ + 'itemid' => $VARS['itemid'], + 'stock' => $VARS['stock'], + 'text1' => $VARS['text1'], + 'userid' => $user + ]; + + $database->insert('stock', $data); + + $currentqty = $database->get('items', 'qty', ['itemid' => $VARS['itemid']]); + $newqty = $currentqty + $VARS['stock']; + + $data = [ + 'qty' => $newqty + ]; + + $database->update('items', $data, ['itemid' => $VARS['itemid']]); + + returnToSender("stock_added"); + case "removestock": + $insert = true; + + if (empty($VARS['stock'])) { + $VARS['stock'] = -1; + } else if (!is_numeric($VARS['stock'])) { + returnToSender('field_nan'); + } + + $user = $_SESSION['uid']; + + $data = [ + 'itemid' => $VARS['itemid'], + 'stock' => -$VARS['stock'], + 'text1' => $VARS['text1'], + 'userid' => $user + ]; + + $database->insert('stock', $data); + + $currentqty = $database->get('items', 'qty', ['itemid' => $VARS['itemid']]); + $newqty = $currentqty - $VARS['stock']; + + $data = [ + 'qty' => $newqty + ]; + + $database->update('items', $data, ['itemid' => $VARS['itemid']]); + + returnToSender("stock_removed"); } diff --git a/database.mwb b/database.mwb index fb8479e..a3c446c 100644 Binary files a/database.mwb and b/database.mwb differ diff --git a/database.sql b/database.sql index 5cac0af..d90c261 100644 --- a/database.sql +++ b/database.sql @@ -1,5 +1,5 @@ -- MySQL Script generated by MySQL Workbench --- Sat 22 Sep 2018 02:40:11 AM MDT +-- Wed 11 Mar 2020 10:06:41 EET -- Model: New Model Version: 1.0 -- MySQL Workbench Forward Engineering @@ -55,14 +55,14 @@ CREATE TABLE IF NOT EXISTS `items` ( `price` DECIMAL(10,2) NULL, PRIMARY KEY (`itemid`), INDEX `fk_items_categories_idx` (`catid` ASC), - INDEX `fk_items_locations1_idx` (`locid` ASC), + INDEX `fk_items_locations_idx` (`locid` ASC), UNIQUE INDEX `itemid_UNIQUE` (`itemid` ASC), CONSTRAINT `fk_items_categories` FOREIGN KEY (`catid`) REFERENCES `categories` (`catid`) ON DELETE NO ACTION ON UPDATE NO ACTION, - CONSTRAINT `fk_items_locations1` + CONSTRAINT `fk_items_locations` FOREIGN KEY (`locid`) REFERENCES `locations` (`locid`) ON DELETE NO ACTION @@ -89,12 +89,12 @@ CREATE TABLE IF NOT EXISTS `permissions` ( `itemid` INT NOT NULL, `canedit` TINYINT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`userid`, `itemid`), - INDEX `fk_permissions_items1_idx` (`itemid` ASC), - CONSTRAINT `fk_permissions_items1` + INDEX `fk_permissions_items_idx` (`itemid` ASC), + CONSTRAINT `fk_permissions_items` FOREIGN KEY (`itemid`) REFERENCES `items` (`itemid`) - ON DELETE NO ACTION - ON UPDATE NO ACTION) + ON DELETE CASCADE + ON UPDATE CASCADE) ENGINE = InnoDB; @@ -120,12 +120,33 @@ CREATE TABLE IF NOT EXISTS `images` ( `primary` TINYINT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`imageid`, `itemid`), UNIQUE INDEX `imageid_UNIQUE` (`imageid` ASC), - INDEX `fk_images_items1_idx` (`itemid` ASC), - CONSTRAINT `fk_images_items1` + INDEX `fk_images_items_idx` (`itemid` ASC), + CONSTRAINT `fk_images_items` FOREIGN KEY (`itemid`) REFERENCES `items` (`itemid`) - ON DELETE NO ACTION - ON UPDATE NO ACTION) + ON DELETE CASCADE + ON UPDATE CASCADE) +ENGINE = InnoDB; + + +-- ----------------------------------------------------- +-- Table `stock` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `stock` ( + `stockid` INT NOT NULL AUTO_INCREMENT, + `itemid` INT NOT NULL, + `stock` INT NOT NULL, + `text1` TEXT(500) NOT NULL, + `userid` INT NOT NULL, + `timestamp` TIMESTAMP NOT NULL, + PRIMARY KEY (`stockid`, `itemid`), + UNIQUE INDEX `stockid_UNIQUE` (`stockid` ASC), + INDEX `fk_stock_items_idx` (`itemid` ASC), + CONSTRAINT `fk_stock_items` + FOREIGN KEY (`itemid`) + REFERENCES `items` (`itemid`) + ON DELETE CASCADE + ON UPDATE CASCADE) ENGINE = InnoDB; diff --git a/langs/en/actions.json b/langs/en/actions.json index 3da53a6..0079195 100644 --- a/langs/en/actions.json +++ b/langs/en/actions.json @@ -4,5 +4,7 @@ "save": "Save", "delete": "Delete", "view": "View", - "show all items": "Show All Items" + "show all items": "Show All Items", + "addstock": "Add", + "removestock": "Remove" } diff --git a/langs/en/core.json b/langs/en/core.json index 4fd38f5..3037784 100644 --- a/langs/en/core.json +++ b/langs/en/core.json @@ -6,7 +6,6 @@ "login server error": "The login server returned an error: {arg}", "login server user data error": "The login server refused to provide account information. Try again or contact technical support.", "captcha error": "There was a problem with the CAPTCHA (robot test). Try again.", - "no access permission": "You do not have permission to access this system.", "no permission": "You do not have permission to access this system.", "no edit permission": "You do not have permission to modify records." } diff --git a/langs/en/items.json b/langs/en/items.json index 15c6549..d824c10 100644 --- a/langs/en/items.json +++ b/langs/en/items.json @@ -9,5 +9,8 @@ "cloning item": "Copying {oitem} {nitem}", "itemid": "Item ID", "id": "ID", - "Edit Item": "Edit Item" + "Edit Item": "Edit Item", + "stockid": "Stock ID", + "adding stock": "Adding stock for {item}", + "removing stock": "Removing stock from {item}" } diff --git a/langs/en/labels.json b/langs/en/labels.json index 1dca287..4af3a80 100644 --- a/langs/en/labels.json +++ b/langs/en/labels.json @@ -7,10 +7,10 @@ "code 1": "Code 1", "code 2": "Code 2", "qty": "Qty", - "want": "Need", - "assigned to": "Assigned To", + "want": "Min", + "assigned to": "Assigned to", "quantity": "Quantity", - "minwant": "Minimum On Hand", + "minwant": "Minimum on hand", "item count": "Item count", "Item cost": "Item cost", "Sale price": "Sale price", @@ -18,5 +18,9 @@ "Notes": "Notes", "Comments": "Comments", "Cost": "Cost", - "Price": "Price" + "Price": "Price", + "date": "Date", + "amount": "Stock amount", + "description": "Description", + "changed by": "Changed by" } diff --git a/langs/en/messages.json b/langs/en/messages.json index c359087..036b246 100644 --- a/langs/en/messages.json +++ b/langs/en/messages.json @@ -17,5 +17,7 @@ "only showing understocked": "Only showing understocked items.", "missing name": "You need to enter a name.", "use the dropdowns": "Whoops, you need to use the category and location autocomplete boxes.", - "make categories and locations": "Please create at least one category and location before adding an item." + "make categories and locations": "Please create at least one category and location before adding an item.", + "stock added": "Stock added.", + "stock removed": "Stock removed." } diff --git a/langs/en/reports.json b/langs/en/reports.json index bb824a3..c76c7f1 100644 --- a/langs/en/reports.json +++ b/langs/en/reports.json @@ -5,5 +5,6 @@ "choose an option": "Choose an option", "csv file": "CSV text file", "ods file": "ODS spreadsheet", - "html file": "HTML web page" + "html file": "HTML web page", + "Stock": "Stock movements" } diff --git a/langs/messages.php b/langs/messages.php index 3155be1..637c3b2 100644 --- a/langs/messages.php +++ b/langs/messages.php @@ -96,5 +96,13 @@ define("MESSAGES", [ "upload_success" => [ "string" => "Image uploaded.", "type" => "success" + ], + "stock_added" => [ + "string" => "stock added", + "type" => "success" + ], + "stock_removed" => [ + "string" => "stock removed", + "type" => "success" ] ]); diff --git a/lib/getitemtable.php b/lib/getitemtable.php index afe32b5..6aee0d5 100644 --- a/lib/getitemtable.php +++ b/lib/getitemtable.php @@ -32,25 +32,25 @@ if ($VARS['order'][0]['dir'] == 'asc') { $sortby = "ASC"; } switch ($VARS['order'][0]['column']) { - case 2: + case 1: $order = ["name" => $sortby]; break; - case 3: + case 2: $order = ["catname" => $sortby]; break; - case 4: + case 3: $order = ["locname" => $sortby]; break; - case 5: + case 4: $order = ["code1" => $sortby]; break; - case 6: + case 5: $order = ["code2" => $sortby]; break; - case 7: + case 6: $order = ["qty" => $sortby]; break; - case 8: + case 7: $order = ["want" => $sortby]; break; // Note: We're not going to sort by assigned user. It's too hard. Maybe later. @@ -118,8 +118,17 @@ for ($i = 0; $i < count($items); $i++) { $user = new User($_SESSION['uid']); if ($user->hasPermission("INV_EDIT")) { $items[$i]["editbtn"] = ' ' . $Strings->get("edit", false) . ''; + if ($SETTINGS['stock_management']) { + $items[$i]["addstockbtn"] = ' ' . $Strings->get("addstock", false) . ''; + $items[$i]["removestockbtn"] = ' ' . $Strings->get("removestock", false) . ''; + } else { + $items[$i]["addstockbtn"] = ''; + $items[$i]["removestockbtn"] = ''; + } } else { $items[$i]["editbtn"] = ''; + $items[$i]["addstockbtn"] = ''; + $items[$i]["removestockbtn"] = ''; } $items[$i]["viewbtn"] = ' ' . $Strings->get("view", false) . ''; if (is_null($items[$i]['userid'])) { diff --git a/lib/reports.php b/lib/reports.php index 182272d..c2e7afc 100644 --- a/lib/reports.php +++ b/lib/reports.php @@ -75,7 +75,7 @@ function getItemReport($filter = []): Report { $Strings->get("code 1", false), $Strings->get("code 2", false), $Strings->get("quantity", false), - $Strings->get("want", false), + $Strings->get("minwant", false), $Strings->get("Cost", false), $Strings->get("Price", false), $Strings->get("assigned to", false), @@ -151,6 +151,49 @@ function getLocationReport(): Report { return $report; } +function getStockReport($filter = []): Report { + global $database, $Strings; + $stock = $database->select( + "stock", [ + "[>]items" => ["itemid"] + ], [ + "stockid", + "itemid", + "name", + 'timestamp', + 'stock', + 'stock.text1', + 'stock.userid' + ]); + $report = new Report($Strings->get("Stock", false)); + $report->setHeader([ + $Strings->get("stockid", false), + $Strings->get("itemid", false), + $Strings->get("name", false), + $Strings->get("date", false), + $Strings->get("amount", false), + $Strings->get("description", false), + $Strings->get("changed by", false) + ]); + for ($i = 0; $i < count($stock); $i++) { + $user = ""; + if (!is_null($stock[$i]["userid"])) { + $u = new User($stock[$i]["userid"]); + $user = $u->getName() . " (" . $u->getUsername() . ')'; + } + $report->addDataRow([ + $stock[$i]["stockid"], + $stock[$i]["itemid"], + $stock[$i]["name"], + $stock[$i]["timestamp"], + $stock[$i]["stock"], + $stock[$i]["text1"], + $user + ]); + } + return $report; +} + function getReport($type): Report { switch ($type) { case "item": @@ -165,6 +208,9 @@ function getReport($type): Report { case "itemstock": return getItemReport(["AND" => ["qty[<]want", "want[>]" => 0]]); break; + case "stock": + return getStockReport(); + break; default: return new Report("error", ["ERROR"], ["Invalid report type."]); } @@ -173,4 +219,4 @@ function getReport($type): Report { function generateReport($type, $format) { $report = getReport($type); $report->output($format); -} \ No newline at end of file +} diff --git a/pages.php b/pages.php index e324a56..f11026d 100644 --- a/pages.php +++ b/pages.php @@ -24,19 +24,6 @@ define("PAGES", [ "static/js/items.js" ], ], - "locations" => [ - "title" => "Locations", - "navbar" => true, - "icon" => "fas fa-map-marker", - "styles" => [ - "static/css/datatables.min.css", - "static/css/tables.css" - ], - "scripts" => [ - "static/js/datatables.min.js", - "static/js/locations.js" - ], - ], "categories" => [ "title" => "Categories", "navbar" => true, @@ -50,6 +37,19 @@ define("PAGES", [ "static/js/categories.js" ], ], + "locations" => [ + "title" => "Locations", + "navbar" => true, + "icon" => "fas fa-map-marker", + "styles" => [ + "static/css/datatables.min.css", + "static/css/tables.css" + ], + "scripts" => [ + "static/js/datatables.min.js", + "static/js/locations.js" + ], + ], "item" => [ "title" => "Item", "navbar" => false @@ -99,5 +99,27 @@ define("PAGES", [ ], "404" => [ "title" => "404 error" + ], + "addstock" => [ + "title" => "Add stock", + "navbar" => false, + "styles" => [ + "static/css/easy-autocomplete.min.css" + ], + "scripts" => [ + "static/js/jquery.easy-autocomplete.min.js", + "static/js/edititem.js" + ], + ], + "removestock" => [ + "title" => "Remove stock", + "navbar" => false, + "styles" => [ + "static/css/easy-autocomplete.min.css" + ], + "scripts" => [ + "static/js/jquery.easy-autocomplete.min.js", + "static/js/edititem.js" + ], ] ]); diff --git a/pages/addstock.php b/pages/addstock.php new file mode 100644 index 0000000..f6e4381 --- /dev/null +++ b/pages/addstock.php @@ -0,0 +1,77 @@ +count("locations") == 0 || $database->count("categories") == 0) { + header('Location: app.php?page=items&msg=noloccat'); + die(); +} + +$itemdata = [ + 'text1' => '', + 'qty' => '']; + +if (!empty($VARS['id'])) { + if ($database->has('items', ['itemid' => $VARS['id']])) { + $itemdata = $database->select( + 'items', [ + 'name', + 'qty', + ], [ + 'itemid' => $VARS['id'] + ])[0]; + } else { + // item id is invalid, redirect to a page that won't cause an error when pressing Save + header('Location: app.php?page=addstock'); + die(); + } +} +?> + +
+
+

+ build("adding stock", ['item' => "" . htmlspecialchars($itemdata['name']) . ""]); ?> +

+
+ +
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + + + '; + } else { + echo ''; + } + ?> + + +
+
diff --git a/pages/categories.php b/pages/categories.php index d76f598..d21df2b 100644 --- a/pages/categories.php +++ b/pages/categories.php @@ -14,9 +14,9 @@ redirectifnotloggedin(); - get('actions'); ?> get('category'); ?> get('item count'); ?> + get('actions'); ?> @@ -30,11 +30,11 @@ redirectifnotloggedin(); ?> - - get("edit"); ?> - + + get("edit"); ?> + - get('actions'); ?> get('category'); ?> get('item count'); ?> + get('actions'); ?> - \ No newline at end of file + diff --git a/pages/edititem.php b/pages/edititem.php index f4da79e..1d2f054 100644 --- a/pages/edititem.php +++ b/pages/edititem.php @@ -164,7 +164,17 @@ if (!empty($VARS['id'])) {
- + + + + +
@@ -263,4 +273,4 @@ if (!empty($VARS['id'])) { ?>
- \ No newline at end of file + diff --git a/pages/export.php b/pages/export.php index f4dde70..089862f 100644 --- a/pages/export.php +++ b/pages/export.php @@ -16,6 +16,13 @@ redirectifnotloggedin();
+
+ + +
+
+
+ + +
+
+
+ + + + + '; + } else { + echo ''; + } + ?> + + + + diff --git a/settings.template.php b/settings.template.php index 4350bd3..d68964a 100644 --- a/settings.template.php +++ b/settings.template.php @@ -56,5 +56,7 @@ $SETTINGS = [ // Base URL for building links relative to the location of the app. // Only used when there's no good context for the path. // The default is almost definitely fine. - "url" => "." + "url" => ".", + // Enable stock management mode. + "stock_management" => false, ]; diff --git a/static/js/categories.js b/static/js/categories.js index 67d6fdd..9384b99 100644 --- a/static/js/categories.js +++ b/static/js/categories.js @@ -24,11 +24,11 @@ $('#cattable').DataTable({ orderable: false }, { - targets: 1, + targets: 3, orderable: false } ], order: [ - [2, 'asc'] + [1, 'asc'] ] -}); \ No newline at end of file +}); diff --git a/static/js/items.js b/static/js/items.js index 298fa14..977dfb9 100644 --- a/static/js/items.js +++ b/static/js/items.js @@ -24,16 +24,16 @@ var itemtable = $('#itemtable').DataTable({ orderable: false }, { - targets: 1, + targets: 8, orderable: false }, { - targets: 8, + targets: 9, orderable: false } ], order: [ - [2, 'asc'] + [1, 'asc'] ], serverSide: true, ajax: { @@ -49,7 +49,6 @@ var itemtable = $('#itemtable').DataTable({ json.items.forEach(function (row) { json.data.push([ "", - "" + row.viewbtn + " " + row.editbtn + "", row.name, row.catname, row.locname + " (" + row.loccode + ")", @@ -57,7 +56,8 @@ var itemtable = $('#itemtable').DataTable({ row.code2, row.qty, row.want, - row.username + row.username, + "" + row.viewbtn + " " + row.editbtn + " " + row.addstockbtn + " " + row.removestockbtn + "" ]); }); return JSON.stringify(json); @@ -74,4 +74,4 @@ $(document).ready(function () { $(searchInput).trigger("input"); $(searchInput).trigger("change"); } -}); \ No newline at end of file +}); diff --git a/static/js/locations.js b/static/js/locations.js index c27465b..eceaaeb 100644 --- a/static/js/locations.js +++ b/static/js/locations.js @@ -24,11 +24,11 @@ $('#loctable').DataTable({ orderable: false }, { - targets: 1, + targets: 4, orderable: false } ], order: [ - [2, 'asc'] + [1, 'asc'] ] -}); \ No newline at end of file +});