Compare commits

..

19 Commits

Author SHA1 Message Date
Bernd Bestel
96209c852c Add flow to add a new product with prefilled barcode 2017-04-21 22:50:16 +02:00
Bernd Bestel
8e40c50cc1 Validate that best before date is min. today 2017-04-21 19:15:03 +02:00
Bernd Bestel
4b0f0141c9 Fix date input arrow key behavior 2017-04-21 19:10:39 +02:00
Bernd Bestel
d1bd21a601 Finished shopping list feature 2017-04-21 19:02:00 +02:00
Bernd Bestel
c6925ba4c3 Started working on shopping list feature 2017-04-21 15:36:04 +02:00
Bernd Bestel
52e311d847 Show missing products on dashboard 2017-04-21 13:21:09 +02:00
Bernd Bestel
f2f18d260d Show how much is in stock on dashboard 2017-04-21 12:50:53 +02:00
Bernd Bestel
1d293741ba Code review 2017-04-21 12:30:08 +02:00
Bernd Bestel
5db288fc3c Add hint when barcode lookup is disabled 2017-04-21 12:03:56 +02:00
Bernd Bestel
d628f9b3ca Make DB migrations fully automatic 2017-04-21 11:52:24 +02:00
Bernd Bestel
fe8a6d96e4 Added keyboard shortcuts for add product/barcode dialogs 2017-04-20 23:42:06 +02:00
Bernd Bestel
bd16b8c851 Added flow to directly add articles and barcodes form purchase and inventory view 2017-04-20 22:01:14 +02:00
Bernd Bestel
c4a22c18f7 This is 1.0 2017-04-20 17:10:21 +02:00
Bernd Bestel
e38c24f9ed Going straight to 1.0... 2017-04-19 21:09:28 +02:00
Bernd Bestel
83a7534a74 Hide barcode in select dropdown but search in it 2017-04-18 23:04:26 +02:00
Bernd Bestel
dc530c80aa Released version 0.4.0 2017-04-18 21:33:44 +02:00
Bernd Bestel
6aaaa2073b Add possibility to have multiple barcodes per product 2017-04-18 21:15:45 +02:00
Bernd Bestel
4f0dc818f6 Make product overview a little bit smaller on large screens 2017-04-18 20:05:02 +02:00
Bernd Bestel
3ed6c9b28a Add a quantity unit conversion hint on product form 2017-04-18 19:54:51 +02:00
38 changed files with 1659 additions and 298 deletions

1
.gitignore vendored
View File

@@ -198,6 +198,5 @@ FakesAssemblies/
/bower_components /bower_components
/vendor /vendor
/.release /.release
/config.php
/composer.phar /composer.phar
/composer.lock /composer.lock

104
Grocy.php Normal file
View File

@@ -0,0 +1,104 @@
<?php
class Grocy
{
private static $DbConnectionRaw;
/**
* @return PDO
*/
public static function GetDbConnectionRaw($doMigrations = false)
{
if ($doMigrations === true)
{
self::$DbConnectionRaw = null;
}
if (self::$DbConnectionRaw == null)
{
$pdo = new PDO('sqlite:' . __DIR__ . '/data/grocy.db');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
if ($doMigrations === true)
{
Grocy::ExecuteDbStatement($pdo, "CREATE TABLE IF NOT EXISTS migrations (migration INTEGER NOT NULL UNIQUE, execution_time_timestamp DATETIME DEFAULT (datetime('now', 'localtime')), PRIMARY KEY(migration)) WITHOUT ROWID");
GrocyDbMigrator::MigrateDb($pdo);
if (self::IsDemoInstallation())
{
GrocyDemoDataGenerator::PopulateDemoData($pdo);
}
}
self::$DbConnectionRaw = $pdo;
}
return self::$DbConnectionRaw;
}
private static $DbConnection;
/**
* @return LessQL\Database
*/
public static function GetDbConnection($doMigrations = false)
{
if ($doMigrations === true)
{
self::$DbConnection = null;
}
if (self::$DbConnection == null)
{
self::$DbConnection = new LessQL\Database(self::GetDbConnectionRaw($doMigrations));
}
return self::$DbConnection;
}
/**
* @return boolean
*/
public static function ExecuteDbStatement(PDO $pdo, string $sql)
{
if ($pdo->exec(utf8_encode($sql)) === false)
{
throw new Exception($pdo->errorInfo());
}
return true;
}
/**
* @return boolean|PDOStatement
*/
public static function ExecuteDbQuery(PDO $pdo, string $sql)
{
if (self::ExecuteDbStatement($pdo, $sql) === true)
{
return $pdo->query(utf8_encode($sql));
}
return false;
}
/**
* @return boolean
*/
public static function IsDemoInstallation()
{
return file_exists(__DIR__ . '/data/demo.txt');
}
private static $InstalledVersion;
/**
* @return string
*/
public static function GetInstalledVersion()
{
if (self::$InstalledVersion == null)
{
self::$InstalledVersion = file_get_contents(__DIR__ . '/version.txt');
}
return self::$InstalledVersion;
}
}

View File

@@ -13,8 +13,10 @@ class GrocyDbMigrator
qu_id_purchase INTEGER NOT NULL, qu_id_purchase INTEGER NOT NULL,
qu_id_stock INTEGER NOT NULL, qu_id_stock INTEGER NOT NULL,
qu_factor_purchase_to_stock REAL NOT NULL, qu_factor_purchase_to_stock REAL NOT NULL,
barcode TEXT UNIQUE, barcode TEXT,
created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) min_stock_amount INTEGER NOT NULL DEFAULT 0,
default_best_before_days INTEGER NOT NULL DEFAULT 0,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)" )"
); );
@@ -23,7 +25,7 @@ class GrocyDbMigrator
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
description TEXT, description TEXT,
created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)" )"
); );
@@ -32,7 +34,7 @@ class GrocyDbMigrator
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
description TEXT, description TEXT,
created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)" )"
); );
@@ -43,20 +45,23 @@ class GrocyDbMigrator
amount INTEGER NOT NULL, amount INTEGER NOT NULL,
best_before_date DATE, best_before_date DATE,
purchased_date DATE DEFAULT (datetime('now', 'localtime')), purchased_date DATE DEFAULT (datetime('now', 'localtime')),
stock_id TEXT NOT NULL stock_id TEXT NOT NULL,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)" )"
); );
self::ExecuteMigrationWhenNeeded($pdo, 5, " self::ExecuteMigrationWhenNeeded($pdo, 5, "
CREATE TABLE consumptions ( CREATE TABLE stock_log (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
product_id INTEGER NOT NULL, product_id INTEGER NOT NULL,
amount INTEGER NOT NULL, amount INTEGER NOT NULL,
best_before_date DATE, best_before_date DATE,
purchased_date DATE, purchased_date DATE,
used_date DATE DEFAULT (datetime('now', 'localtime')), used_date DATE,
spoiled INTEGER NOT NULL DEFAULT 0, spoiled INTEGER NOT NULL DEFAULT 0,
stock_id TEXT NOT NULL stock_id TEXT NOT NULL,
transaction_type TEXT NOT NULL,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)" )"
); );
@@ -66,18 +71,46 @@ class GrocyDbMigrator
INSERT INTO products (name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('DefaultProduct1', 'This is the first default product, edit or delete it', 1, 1, 1, 1); INSERT INTO products (name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('DefaultProduct1', 'This is the first default product, edit or delete it', 1, 1, 1, 1);
INSERT INTO products (name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('DefaultProduct2', 'This is the second default product, edit or delete it', 1, 1, 1, 1);" INSERT INTO products (name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('DefaultProduct2', 'This is the second default product, edit or delete it', 1, 1, 1, 1);"
); );
self::ExecuteMigrationWhenNeeded($pdo, 7, "
CREATE VIEW stock_missing_products
AS
SELECT p.id, MAX(p.name) AS name, p.min_stock_amount - IFNULL(SUM(s.amount), 0) AS amount_missing
FROM products p
LEFT JOIN stock s
ON p.id = s.product_id
WHERE p.min_stock_amount != 0
GROUP BY p.id
HAVING IFNULL(SUM(s.amount), 0) < p.min_stock_amount;"
);
self::ExecuteMigrationWhenNeeded($pdo, 8, "
CREATE VIEW stock_current
AS
SELECT product_id, SUM(amount) AS amount, MIN(best_before_date) AS best_before_date
from stock
GROUP BY product_id
ORDER BY MIN(best_before_date) ASC;"
);
self::ExecuteMigrationWhenNeeded($pdo, 9, "
CREATE TABLE shopping_list (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
product_id INTEGER NOT NULL UNIQUE,
amount INTEGER NOT NULL DEFAULT 0,
amount_autoadded INTEGER NOT NULL DEFAULT 0,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)"
);
} }
private static function ExecuteMigrationWhenNeeded(PDO $pdo, int $migrationId, string $sql) private static function ExecuteMigrationWhenNeeded(PDO $pdo, int $migrationId, string $sql)
{ {
if ($pdo->query("SELECT COUNT(*) FROM migrations WHERE migration = $migrationId")->fetchColumn() == 0) $rowCount = Grocy::ExecuteDbQuery($pdo, 'SELECT COUNT(*) FROM migrations WHERE migration = ' . $migrationId)->fetchColumn();
if (intval($rowCount) === 0)
{ {
if ($pdo->exec(utf8_encode($sql)) === false) Grocy::ExecuteDbStatement($pdo, $sql);
{ Grocy::ExecuteDbStatement($pdo, 'INSERT INTO migrations (migration) VALUES (' . $migrationId . ')');
throw new Exception($pdo->errorInfo());
}
$pdo->exec('INSERT INTO migrations (migration) VALUES (' . $migrationId . ')');
} }
} }
} }

View File

@@ -3,27 +3,57 @@
class GrocyDemoDataGenerator class GrocyDemoDataGenerator
{ {
public static function PopulateDemoData(PDO $pdo) public static function PopulateDemoData(PDO $pdo)
{
$rowCount = Grocy::ExecuteDbQuery($pdo, 'SELECT COUNT(*) FROM migrations WHERE migration = -1')->fetchColumn();
if (intval($rowCount) === 0)
{ {
$sql = " $sql = "
UPDATE locations SET name = 'Vorratskammer', description = '' WHERE id = 1; UPDATE locations SET name = 'Vorratskammer', description = '' WHERE id = 1;
INSERT INTO locations (name) VALUES ('S<><53>igkeitenschrank'); INSERT INTO locations (name) VALUES ('S<><53>igkeitenschrank'); --2
INSERT INTO locations (name) VALUES ('Konvervenschrank'); INSERT INTO locations (name) VALUES ('Konservenschrank'); --3
INSERT INTO locations (name) VALUES ('K<>hlschrank'); --4
UPDATE quantity_units SET name = 'St<53>ck' WHERE id = 1; UPDATE quantity_units SET name = 'St<53>ck' WHERE id = 1;
INSERT INTO quantity_units (name) VALUES ('Packung'); INSERT INTO quantity_units (name) VALUES ('Packung'); --2
INSERT INTO quantity_units (name) VALUES ('Glas'); --3
INSERT INTO quantity_units (name) VALUES ('Dose'); --4
INSERT INTO quantity_units (name) VALUES ('Becher'); --5
INSERT INTO quantity_units (name) VALUES ('Bund'); --6
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Gummib<69>rchen', 2, 2, 2, 1); DELETE FROM products WHERE id IN (1, 2);
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Chips', 2, 2, 2, 1); INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount) VALUES ('Gummib<69>rchen', 2, 2, 2, 1, 8); --3
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Eier', 1, 2, 1, 10); INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount) VALUES ('Chips', 2, 2, 2, 1, 10); --4
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Eier', 1, 2, 1, 10); --5
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Nudeln', 1, 2, 2, 1); --6
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Essiggurken', 3, 3, 3, 1); --7
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Gulaschsuppe', 3, 4, 4, 1); --8
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Joghurt', 4, 5, 5, 1); --9
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('K<>se', 4, 2, 2, 1); --10
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Aufschnitt', 4, 2, 2, 1); --11
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Paprika', 4, 1, 1, 1); --12
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Gurke', 4, 1, 1, 1); --13
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Radieschen', 4, 6, 6, 1); --14
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Tomate', 4, 1, 1, 1); --15
INSERT INTO stock (product_id, amount, best_before_date, stock_id) VALUES (3, 5, date('now', '+180 day'), '".uniqid()."'); INSERT INTO migrations (migration) VALUES (-1);
INSERT INTO stock (product_id, amount, best_before_date, stock_id) VALUES (4, 5, date('now', '+180 day'), '".uniqid()."');
INSERT INTO stock (product_id, amount, best_before_date, stock_id) VALUES (5, 5, date('now', '+25 day'), '".uniqid()."');
"; ";
if ($pdo->exec(utf8_encode($sql)) === false) Grocy::ExecuteDbStatement($pdo, $sql);
{
throw new Exception($pdo->errorInfo()); GrocyLogicStock::AddProduct(3, 5, date('Y-m-d', strtotime('+180 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(4, 5, date('Y-m-d', strtotime('+180 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(5, 5, date('Y-m-d', strtotime('+20 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(6, 5, date('Y-m-d', strtotime('+600 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(7, 5, date('Y-m-d', strtotime('+800 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(8, 5, date('Y-m-d', strtotime('+900 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(9, 5, date('Y-m-d', strtotime('+14 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(10, 5, date('Y-m-d', strtotime('+21 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(11, 5, date('Y-m-d', strtotime('+10 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(12, 5, date('Y-m-d', strtotime('+2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(13, 5, date('Y-m-d', strtotime('-2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(14, 5, date('Y-m-d', strtotime('+2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(15, 5, date('Y-m-d', strtotime('-2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddMissingProductsToShoppingList();
} }
} }
} }

View File

@@ -2,10 +2,20 @@
class GrocyLogicStock class GrocyLogicStock
{ {
const TRANSACTION_TYPE_PURCHASE = 'purchase';
const TRANSACTION_TYPE_CONSUME = 'consume';
const TRANSACTION_TYPE_INVENTORY_CORRECTION = 'inventory-correction';
public static function GetCurrentStock() public static function GetCurrentStock()
{ {
$db = Grocy::GetDbConnectionRaw(); $sql = 'SELECT * from stock_current';
return $db->query('SELECT product_id, SUM(amount) AS amount, MIN(best_before_date) AS best_before_date from stock GROUP BY product_id ORDER BY MIN(best_before_date) ASC')->fetchAll(PDO::FETCH_OBJ); return Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), $sql)->fetchAll(PDO::FETCH_OBJ);
}
public static function GetMissingProducts()
{
$sql = 'SELECT * from stock_missing_products';
return Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), $sql)->fetchAll(PDO::FETCH_OBJ);
} }
public static function GetProductDetails(int $productId) public static function GetProductDetails(int $productId)
@@ -15,7 +25,7 @@ class GrocyLogicStock
$product = $db->products($productId); $product = $db->products($productId);
$productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount'); $productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount');
$productLastPurchased = $db->stock()->where('product_id', $productId)->max('purchased_date'); $productLastPurchased = $db->stock()->where('product_id', $productId)->max('purchased_date');
$productLastUsed = $db->consumptions()->where('product_id', $productId)->max('used_date'); $productLastUsed = $db->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->max('used_date');
$quPurchase = $db->quantity_units($product->qu_id_purchase); $quPurchase = $db->quantity_units($product->qu_id_purchase);
$quStock = $db->quantity_units($product->qu_id_stock); $quStock = $db->quantity_units($product->qu_id_stock);
@@ -29,7 +39,43 @@ class GrocyLogicStock
); );
} }
public static function ConsumeProduct(int $productId, int $amount, bool $spoiled) public static function AddProduct(int $productId, int $amount, string $bestBeforeDate, $transactionType)
{
if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_PURCHASE || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION)
{
$db = Grocy::GetDbConnection();
$stockId = uniqid();
$logRow = $db->stock_log()->createRow(array(
'product_id' => $productId,
'amount' => $amount,
'best_before_date' => $bestBeforeDate,
'purchased_date' => date('Y-m-d'),
'stock_id' => $stockId,
'transaction_type' => $transactionType
));
$logRow->save();
$stockRow = $db->stock()->createRow(array(
'product_id' => $productId,
'amount' => $amount,
'best_before_date' => $bestBeforeDate,
'purchased_date' => date('Y-m-d'),
'stock_id' => $stockId,
));
$stockRow->save();
return true;
}
else
{
throw new Exception("Transaction type $transactionType is not valid (GrocyLogicStock.AddProduct)");
}
}
public static function ConsumeProduct(int $productId, int $amount, bool $spoiled, $transactionType)
{
if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_PURCHASE || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION)
{ {
$db = Grocy::GetDbConnection(); $db = Grocy::GetDbConnection();
@@ -50,30 +96,34 @@ class GrocyLogicStock
if ($amount >= $stockEntry->amount) //Take the whole stock entry if ($amount >= $stockEntry->amount) //Take the whole stock entry
{ {
$consumptionRow = $db->consumptions()->createRow(array( $logRow = $db->stock_log()->createRow(array(
'product_id' => $stockEntry->product_id, 'product_id' => $stockEntry->product_id,
'amount' => $stockEntry->amount, 'amount' => $stockEntry->amount * -1,
'best_before_date' => $stockEntry->best_before_date, 'best_before_date' => $stockEntry->best_before_date,
'purchased_date' => $stockEntry->purchased_date, 'purchased_date' => $stockEntry->purchased_date,
'used_date' => date('Y-m-d'),
'spoiled' => $spoiled, 'spoiled' => $spoiled,
'stock_id' => $stockEntry->stock_id 'stock_id' => $stockEntry->stock_id,
'transaction_type' => $transactionType
)); ));
$consumptionRow->save(); $logRow->save();
$amount -= $stockEntry->amount; $amount -= $stockEntry->amount;
$stockEntry->delete(); $stockEntry->delete();
} }
else //Stock entry amount is > than needed amount -> split the stock entry resp. update the amount else //Stock entry amount is > than needed amount -> split the stock entry resp. update the amount
{ {
$consumptionRow = $db->consumptions()->createRow(array( $logRow = $db->stock_log()->createRow(array(
'product_id' => $stockEntry->product_id, 'product_id' => $stockEntry->product_id,
'amount' => $amount, 'amount' => $amount * -1,
'best_before_date' => $stockEntry->best_before_date, 'best_before_date' => $stockEntry->best_before_date,
'purchased_date' => $stockEntry->purchased_date, 'purchased_date' => $stockEntry->purchased_date,
'used_date' => date('Y-m-d'),
'spoiled' => $spoiled, 'spoiled' => $spoiled,
'stock_id' => $stockEntry->stock_id 'stock_id' => $stockEntry->stock_id,
'transaction_type' => $transactionType
)); ));
$consumptionRow->save(); $logRow->save();
$restStockAmount = $stockEntry->amount - $amount; $restStockAmount = $stockEntry->amount - $amount;
$amount = 0; $amount = 0;
@@ -86,4 +136,56 @@ class GrocyLogicStock
return true; return true;
} }
else
{
throw new Exception("Transaction type $transactionType is not valid (GrocyLogicStock.ConsumeProduct)");
}
}
public static function InventoryProduct(int $productId, int $newAmount, string $bestBeforeDate)
{
$db = Grocy::GetDbConnection();
$productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount');
if ($newAmount > $productStockAmount)
{
$amountToAdd = $newAmount - $productStockAmount;
self::AddProduct($productId, $amountToAdd, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION);
}
else if ($newAmount < $productStockAmount)
{
$amountToRemove = $productStockAmount - $newAmount;
self::ConsumeProduct($productId, $amountToRemove, false, self::TRANSACTION_TYPE_INVENTORY_CORRECTION);
}
return true;
}
public static function AddMissingProductsToShoppingList()
{
$db = Grocy::GetDbConnection();
$missingProducts = self::GetMissingProducts();
foreach ($missingProducts as $missingProduct)
{
$product = $db->products()->where('id', $missingProduct->id)->fetch();
$amount = ceil($missingProduct->amount_missing / $product->qu_factor_purchase_to_stock);
$alreadyExistingEntry = $db->shopping_list()->where('product_id', $missingProduct->id)->fetch();
if ($alreadyExistingEntry) //Update
{
$alreadyExistingEntry->update(array(
'amount_autoadded' => $amount
));
}
else //Insert
{
$shoppinglistRow = $db->shopping_list()->createRow(array(
'product_id' => $missingProduct->id,
'amount_autoadded' => $amount
));
$shoppinglistRow->save();
}
}
}
} }

View File

@@ -14,4 +14,47 @@ class GrocyPhpHelper
return null; return null;
} }
public static function FindAllObjectsInArrayByPropertyValue($array, $propertyName, $propertyValue, $operator = '==')
{
$returnArray = array();
foreach($array as $object)
{
switch($operator)
{
case '==':
if($object->{$propertyName} == $propertyValue)
{
$returnArray[] = $object;
}
break;
case '>':
if($object->{$propertyName} > $propertyValue)
{
$returnArray[] = $object;
}
break;
case '<':
if($object->{$propertyName} < $propertyValue)
{
$returnArray[] = $object;
}
break;
}
}
return $returnArray;
}
public static function SumArrayValue($array, $propertyName)
{
$sum = 0;
foreach($array as $object)
{
$sum += $object->{$propertyName};
}
return $sum;
}
} }

View File

@@ -11,10 +11,7 @@ For now my main focus is on stock management, ERP your fridge!
Public demo of the latest version &rarr; [https://grocy.projectdemos.berrnd.org](https://grocy.projectdemos.berrnd.org) Public demo of the latest version &rarr; [https://grocy.projectdemos.berrnd.org](https://grocy.projectdemos.berrnd.org)
## How to install ## How to install
Just unpack the [latest release](https://github.com/berrnd/grocy/releases/latest) on your PHP enabled webserver, copy `config-dist.php` to `config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go. Alternatively clone this repository and install Composer and Bower dependencies manually. Just unpack the [latest release](https://github.com/berrnd/grocy/releases/latest) on your PHP enabled webserver, copy `config-dist.php` to `data/config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go. Alternatively clone this repository and install Composer and Bower dependencies manually.
## Todo
A lot...
## License ## License
The MIT License (MIT) The MIT License (MIT)

View File

@@ -15,6 +15,7 @@
"datatables.net-responsive": "2.1.1", "datatables.net-responsive": "2.1.1",
"datatables.net-responsive-bs": "2.1.1", "datatables.net-responsive-bs": "2.1.1",
"jquery-timeago": "1.5.4", "jquery-timeago": "1.5.4",
"toastr": "2.1.3" "toastr": "2.1.3",
"tagmanager": "3.0.2"
} }
} }

View File

@@ -7,5 +7,5 @@ mkdir "%releasePath%"
for /f "tokens=*" %%a in ('type version.txt') do set version=%%a for /f "tokens=*" %%a in ('type version.txt') do set version=%%a
del "%releasePath%\grocy_%version%.zip" del "%releasePath%\grocy_%version%.zip"
"build_tools\7za.exe" a -r "%releasePath%\grocy_%version%.zip" "%projectPath%\*" -xr!.* -xr!build_tools -xr!build.bat -xr!composer.json -xr!composer.lock -xr!composer.phar -xr!grocy.phpproj -xr!grocy.phpproj.user -xr!grocy.sln "build_tools\7za.exe" a -r "%releasePath%\grocy_%version%.zip" "%projectPath%\*" -xr!.* -xr!build_tools -xr!build.bat -xr!composer.json -xr!composer.lock -xr!composer.phar -xr!grocy.phpproj -xr!grocy.phpproj.user -xr!grocy.sln -xr!bower.json
"build_tools\7za.exe" d "%releasePath%\grocy_%version%.zip" data\add_before_end_body.html data\demo.txt data\grocy.db data\.gitignore config.php bower.json "build_tools\7za.exe" d "%releasePath%\grocy_%version%.zip" data\*.*

View File

@@ -75,3 +75,29 @@ Grocy.EmptyElementWhenMatches = function(selector, text)
$(selector).text(''); $(selector).text('');
} }
}; };
String.prototype.contains = function(search)
{
return this.toLowerCase().indexOf(search.toLowerCase()) !== -1;
};
Grocy.GetUriParam = function(key)
{
var currentUri = decodeURIComponent(window.location.search.substring(1));
var vars = currentUri.split('&');
for (i = 0; i < vars.length; i++)
{
var currentParam = vars[i].split('=');
if (currentParam[0] === key)
{
return currentParam[1] === undefined ? true : currentParam[1];
}
}
};
Grocy.Wait = function(ms)
{
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@@ -1,63 +0,0 @@
<?php
class Grocy
{
private static $DbConnectionRaw;
/**
* @return PDO
*/
public static function GetDbConnectionRaw()
{
if (self::$DbConnectionRaw == null)
{
$newDb = !file_exists(__DIR__ . '/data/grocy.db');
$pdo = new PDO('sqlite:' . __DIR__ . '/data/grocy.db');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
if ($newDb)
{
$pdo->exec("CREATE TABLE migrations (migration INTEGER NOT NULL UNIQUE, execution_time_timestamp DATETIME DEFAULT (datetime('now', 'localtime')), PRIMARY KEY(migration)) WITHOUT ROWID");
GrocyDbMigrator::MigrateDb($pdo);
if (self::IsDemoInstallation())
{
GrocyDemoDataGenerator::PopulateDemoData($pdo);
}
}
self::$DbConnectionRaw = $pdo;
}
return self::$DbConnectionRaw;
}
private static $DbConnection;
/**
* @return LessQL\Database
*/
public static function GetDbConnection()
{
if (self::$DbConnection == null)
{
self::$DbConnection = new LessQL\Database(self::GetDbConnectionRaw());
}
return self::$DbConnection;
}
public static function IsDemoInstallation()
{
return file_exists(__DIR__ . '/data/demo.txt');
}
private static $InstalledVersion;
public static function GetInstalledVersion()
{
if (self::$InstalledVersion == null)
{
self::$InstalledVersion = file_get_contents(__DIR__ . '/version.txt');
}
return self::$InstalledVersion;
}
}

View File

@@ -24,6 +24,9 @@
<Compile Include="GrocyDbMigrator.php" /> <Compile Include="GrocyDbMigrator.php" />
<Compile Include="index.php" /> <Compile Include="index.php" />
<Compile Include="views\consumption.php" /> <Compile Include="views\consumption.php" />
<Compile Include="views\inventory.php" />
<Compile Include="views\shoppinglistform.php" />
<Compile Include="views\shoppinglist.php" />
<Compile Include="views\purchase.php" /> <Compile Include="views\purchase.php" />
<Compile Include="views\quantityunitform.php" /> <Compile Include="views\quantityunitform.php" />
<Compile Include="views\locationform.php" /> <Compile Include="views\locationform.php" />
@@ -49,6 +52,9 @@
<Content Include="version.txt" /> <Content Include="version.txt" />
<Content Include="views\consumption.js" /> <Content Include="views\consumption.js" />
<Content Include="views\dashboard.js" /> <Content Include="views\dashboard.js" />
<Content Include="views\inventory.js" />
<Content Include="views\shoppinglistform.js" />
<Content Include="views\shoppinglist.js" />
<Content Include="views\purchase.js" /> <Content Include="views\purchase.js" />
<Content Include="views\quantityunitform.js" /> <Content Include="views\quantityunitform.js" />
<Content Include="views\locationform.js" /> <Content Include="views\locationform.js" />

183
index.php
View File

@@ -4,13 +4,13 @@ use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response; use \Psr\Http\Message\ResponseInterface as Response;
use Slim\Views\PhpRenderer; use Slim\Views\PhpRenderer;
require_once 'vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
require_once 'config.php'; require_once __DIR__ . '/data/config.php';
require_once 'Grocy.php'; require_once __DIR__ . '/Grocy.php';
require_once 'GrocyDbMigrator.php'; require_once __DIR__ . '/GrocyDbMigrator.php';
require_once 'GrocyDemoDataGenerator.php'; require_once __DIR__ . '/GrocyDemoDataGenerator.php';
require_once 'GrocyLogicStock.php'; require_once __DIR__ . '/GrocyLogicStock.php';
require_once 'GrocyPhpHelper.php'; require_once __DIR__ . '/GrocyPhpHelper.php';
$app = new \Slim\App(new \Slim\Container([ $app = new \Slim\App(new \Slim\Container([
'settings' => [ 'settings' => [
@@ -32,22 +32,23 @@ if (!Grocy::IsDemoInstallation())
])); ]));
} }
$app->get('/', function(Request $request, Response $response)
{
$db = Grocy::GetDbConnection(); $db = Grocy::GetDbConnection();
$app->get('/', function(Request $request, Response $response) use($db)
{
$db = Grocy::GetDbConnection(true); //For database schema migration
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
'title' => 'Dashboard', 'title' => 'Dashboard',
'contentPage' => 'dashboard.php', 'contentPage' => 'dashboard.php',
'products' => $db->products(), 'products' => $db->products(),
'currentStock' => GrocyLogicStock::GetCurrentStock() 'currentStock' => GrocyLogicStock::GetCurrentStock(),
'missingProducts' => GrocyLogicStock::GetMissingProducts()
]); ]);
}); });
$app->get('/purchase', function(Request $request, Response $response) $app->get('/purchase', function(Request $request, Response $response) use($db)
{ {
$db = Grocy::GetDbConnection();
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
'title' => 'Purchase', 'title' => 'Purchase',
'contentPage' => 'purchase.php', 'contentPage' => 'purchase.php',
@@ -55,10 +56,8 @@ $app->get('/purchase', function(Request $request, Response $response)
]); ]);
}); });
$app->get('/consumption', function(Request $request, Response $response) $app->get('/consumption', function(Request $request, Response $response) use($db)
{ {
$db = Grocy::GetDbConnection();
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
'title' => 'Consumption', 'title' => 'Consumption',
'contentPage' => 'consumption.php', 'contentPage' => 'consumption.php',
@@ -66,10 +65,29 @@ $app->get('/consumption', function(Request $request, Response $response)
]); ]);
}); });
$app->get('/products', function(Request $request, Response $response) $app->get('/inventory', function(Request $request, Response $response) use($db)
{ {
$db = Grocy::GetDbConnection(); return $this->renderer->render($response, '/layout.php', [
'title' => 'Inventory',
'contentPage' => 'inventory.php',
'products' => $db->products()
]);
});
$app->get('/shoppinglist', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Shopping list',
'contentPage' => 'shoppinglist.php',
'listItems' => $db->shopping_list(),
'products' => $db->products(),
'quantityunits' => $db->quantity_units(),
'missingProducts' => GrocyLogicStock::GetMissingProducts()
]);
});
$app->get('/products', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
'title' => 'Products', 'title' => 'Products',
'contentPage' => 'products.php', 'contentPage' => 'products.php',
@@ -79,10 +97,8 @@ $app->get('/products', function(Request $request, Response $response)
]); ]);
}); });
$app->get('/locations', function(Request $request, Response $response) $app->get('/locations', function(Request $request, Response $response) use($db)
{ {
$db = Grocy::GetDbConnection();
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
'title' => 'Locations', 'title' => 'Locations',
'contentPage' => 'locations.php', 'contentPage' => 'locations.php',
@@ -90,10 +106,8 @@ $app->get('/locations', function(Request $request, Response $response)
]); ]);
}); });
$app->get('/quantityunits', function(Request $request, Response $response) $app->get('/quantityunits', function(Request $request, Response $response) use($db)
{ {
$db = Grocy::GetDbConnection();
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
'title' => 'Quantity units', 'title' => 'Quantity units',
'contentPage' => 'quantityunits.php', 'contentPage' => 'quantityunits.php',
@@ -101,10 +115,8 @@ $app->get('/quantityunits', function(Request $request, Response $response)
]); ]);
}); });
$app->get('/product/{productId}', function(Request $request, Response $response, $args) $app->get('/product/{productId}', function(Request $request, Response $response, $args) use($db)
{ {
$db = Grocy::GetDbConnection();
if ($args['productId'] == 'new') if ($args['productId'] == 'new')
{ {
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
@@ -128,10 +140,8 @@ $app->get('/product/{productId}', function(Request $request, Response $response,
} }
}); });
$app->get('/location/{locationId}', function(Request $request, Response $response, $args) $app->get('/location/{locationId}', function(Request $request, Response $response, $args) use($db)
{ {
$db = Grocy::GetDbConnection();
if ($args['locationId'] == 'new') if ($args['locationId'] == 'new')
{ {
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
@@ -151,10 +161,8 @@ $app->get('/location/{locationId}', function(Request $request, Response $respons
} }
}); });
$app->get('/quantityunit/{quantityunitId}', function(Request $request, Response $response, $args) $app->get('/quantityunit/{quantityunitId}', function(Request $request, Response $response, $args) use($db)
{ {
$db = Grocy::GetDbConnection();
if ($args['quantityunitId'] == 'new') if ($args['quantityunitId'] == 'new')
{ {
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
@@ -174,67 +182,80 @@ $app->get('/quantityunit/{quantityunitId}', function(Request $request, Response
} }
}); });
$app->group('/api', function() $app->get('/shoppinglist/{itemId}', function(Request $request, Response $response, $args) use($db)
{ {
$this->get('/get-objects/{entity}', function(Request $request, Response $response, $args) if ($args['itemId'] == 'new')
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Add shopping list item',
'contentPage' => 'shoppinglistform.php',
'products' => $db->products(),
'mode' => 'create'
]);
}
else
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Edit shopping list item',
'contentPage' => 'shoppinglistform.php',
'listItem' => $db->shopping_list($args['itemId']),
'products' => $db->products(),
'mode' => 'edit'
]);
}
});
$app->group('/api', function() use($db)
{
$this->get('/get-objects/{entity}', function(Request $request, Response $response, $args) use($db)
{ {
$db = Grocy::GetDbConnection();
echo json_encode($db->{$args['entity']}()); echo json_encode($db->{$args['entity']}());
return $response->withHeader('Content-Type', 'application/json');
}); });
$this->get('/get-object/{entity}/{objectId}', function(Request $request, Response $response, $args) $this->get('/get-object/{entity}/{objectId}', function(Request $request, Response $response, $args) use($db)
{ {
$db = Grocy::GetDbConnection();
echo json_encode($db->{$args['entity']}($args['objectId'])); echo json_encode($db->{$args['entity']}($args['objectId']));
return $response->withHeader('Content-Type', 'application/json');
}); });
$this->post('/add-object/{entity}', function(Request $request, Response $response, $args) $this->post('/add-object/{entity}', function(Request $request, Response $response, $args) use($db)
{ {
$db = Grocy::GetDbConnection();
$newRow = $db->{$args['entity']}()->createRow($request->getParsedBody()); $newRow = $db->{$args['entity']}()->createRow($request->getParsedBody());
$newRow->save(); $newRow->save();
$success = $newRow->isClean(); $success = $newRow->isClean();
echo json_encode(array('success' => $success)); echo json_encode(array('success' => $success));
return $response->withHeader('Content-Type', 'application/json');
}); });
$this->post('/edit-object/{entity}/{objectId}', function(Request $request, Response $response, $args) $this->post('/edit-object/{entity}/{objectId}', function(Request $request, Response $response, $args) use($db)
{ {
$db = Grocy::GetDbConnection();
$row = $db->{$args['entity']}($args['objectId']); $row = $db->{$args['entity']}($args['objectId']);
$row->update($request->getParsedBody()); $row->update($request->getParsedBody());
$success = $row->isClean(); $success = $row->isClean();
echo json_encode(array('success' => $success)); echo json_encode(array('success' => $success));
return $response->withHeader('Content-Type', 'application/json');
}); });
$this->get('/delete-object/{entity}/{objectId}', function(Request $request, Response $response, $args) $this->get('/delete-object/{entity}/{objectId}', function(Request $request, Response $response, $args) use($db)
{ {
$db = Grocy::GetDbConnection();
$row = $db->{$args['entity']}($args['objectId']); $row = $db->{$args['entity']}($args['objectId']);
$row->delete(); $row->delete();
$success = $row->isClean(); $success = $row->isClean();
echo json_encode(array('success' => $success)); echo json_encode(array('success' => $success));
return $response->withHeader('Content-Type', 'application/json');
}); });
$this->get('/stock/get-product-details/{productId}', function(Request $request, Response $response, $args) $this->get('/stock/add-product/{productId}/{amount}', function(Request $request, Response $response, $args)
{ {
echo json_encode(GrocyLogicStock::GetProductDetails($args['productId'])); $bestBeforeDate = date('Y-m-d');
return $response->withHeader('Content-Type', 'application/json'); if (isset($request->getQueryParams()['bestbeforedate']) && !empty($request->getQueryParams()['bestbeforedate']))
}); {
$bestBeforeDate = $request->getQueryParams()['bestbeforedate'];
}
$this->get('/stock/get-current-stock', function(Request $request, Response $response) $transactionType = GrocyLogicStock::TRANSACTION_TYPE_PURCHASE;
if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype']))
{ {
echo json_encode(GrocyLogicStock::GetCurrentStock()); $transactionType = $request->getQueryParams()['transactiontype'];
return $response->withHeader('Content-Type', 'application/json'); }
echo json_encode(array('success' => GrocyLogicStock::AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType)));
}); });
$this->get('/stock/consume-product/{productId}/{amount}', function(Request $request, Response $response, $args) $this->get('/stock/consume-product/{productId}/{amount}', function(Request $request, Response $response, $args)
@@ -245,15 +266,45 @@ $app->group('/api', function()
$spoiled = true; $spoiled = true;
} }
echo json_encode(array('success' => GrocyLogicStock::ConsumeProduct($args['productId'], $args['amount'], $spoiled))); $transactionType = GrocyLogicStock::TRANSACTION_TYPE_CONSUME;
return $response->withHeader('Content-Type', 'application/json'); if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype']))
{
$transactionType = $request->getQueryParams()['transactiontype'];
}
echo json_encode(array('success' => GrocyLogicStock::ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType)));
}); });
$this->get('/helper/uniqid', function(Request $request, Response $response) $this->get('/stock/inventory-product/{productId}/{newAmount}', function(Request $request, Response $response, $args)
{ {
echo json_encode(array('uniqid' => uniqid())); $bestBeforeDate = date('Y-m-d');
return $response->withHeader('Content-Type', 'application/json'); if (isset($request->getQueryParams()['bestbeforedate']) && !empty($request->getQueryParams()['bestbeforedate']))
{
$bestBeforeDate = $request->getQueryParams()['bestbeforedate'];
}
echo json_encode(array('success' => GrocyLogicStock::InventoryProduct($args['productId'], $args['newAmount'], $bestBeforeDate)));
}); });
$this->get('/stock/get-product-details/{productId}', function(Request $request, Response $response, $args)
{
echo json_encode(GrocyLogicStock::GetProductDetails($args['productId']));
});
$this->get('/stock/get-current-stock', function(Request $request, Response $response)
{
echo json_encode(GrocyLogicStock::GetCurrentStock());
});
$this->get('/stock/add-missing-products-to-shoppinglist', function(Request $request, Response $response)
{
GrocyLogicStock::AddMissingProductsToShoppingList();
echo json_encode(array('success' => true));
});
})->add(function($request, $response, $next)
{
$response = $next($request, $response);
return $response->withHeader('Content-Type', 'application/json');
}); });
$app->run(); $app->run();

View File

@@ -106,6 +106,29 @@
font-size: 0.8em; font-size: 0.8em;
} }
.disabled { .disabled,
.no-real-button {
pointer-events: none; pointer-events: none;
} }
.warning-bg {
background-color: #fcf8e3 !important;
}
.error-bg {
background-color: #f2dede !important;
}
.info-bg {
background-color: #afd9ee !important;
}
.discrete-content-separator {
padding-top: 5px;
padding-bottom: 5px;
}
.discrete-content-separator-2x {
padding-top: 10px;
padding-bottom: 10px;
}

View File

@@ -1 +1 @@
0.3.0 1.2.0

View File

@@ -45,22 +45,24 @@ $('#product_id').on('change', function(e)
if (productId) if (productId)
{ {
Grocy.FetchJson('/api/stock/get-product-details/' + productId, Grocy.FetchJson('/api/stock/get-product-details/' + productId,
function(productStatistics) function (productDetails)
{ {
$('#selected-product-name').text(productStatistics.product.name); $('#selected-product-name').text(productDetails.product.name);
$('#selected-product-stock-amount').text(productStatistics.stock_amount || '0'); $('#selected-product-stock-amount').text(productDetails.stock_amount || '0');
$('#selected-product-stock-qu-name').text(productStatistics.quantity_unit_stock.name); $('#selected-product-stock-qu-name').text(productDetails.quantity_unit_stock.name);
$('#selected-product-stock-qu-name2').text(productStatistics.quantity_unit_stock.name); $('#selected-product-stock-qu-name2').text(productDetails.quantity_unit_stock.name);
$('#selected-product-last-purchased').text((productStatistics.last_purchased || 'never').substring(0, 10)); $('#selected-product-last-purchased').text((productDetails.last_purchased || 'never').substring(0, 10));
$('#selected-product-last-purchased-timeago').text($.timeago(productStatistics.last_purchased || '')); $('#selected-product-last-purchased-timeago').text($.timeago(productDetails.last_purchased || ''));
$('#selected-product-last-used').text((productStatistics.last_used || 'never').substring(0, 10)); $('#selected-product-last-used').text((productDetails.last_used || 'never').substring(0, 10));
$('#selected-product-last-used-timeago').text($.timeago(productStatistics.last_used || '')); $('#selected-product-last-used-timeago').text($.timeago(productDetails.last_used || ''));
$('#amount').attr('max', productStatistics.stock_amount); $('#amount').attr('max', productDetails.stock_amount);
$('#consumption-form').validator('update');
$('#amount_qu_unit').text(productDetails.quantity_unit_stock.name);
Grocy.EmptyElementWhenMatches('#selected-product-last-purchased-timeago', 'NaN years ago'); Grocy.EmptyElementWhenMatches('#selected-product-last-purchased-timeago', 'NaN years ago');
Grocy.EmptyElementWhenMatches('#selected-product-last-used-timeago', 'NaN years ago'); Grocy.EmptyElementWhenMatches('#selected-product-last-used-timeago', 'NaN years ago');
if ((productStatistics.stock_amount || 0) === 0) if ((productDetails.stock_amount || 0) === 0)
{ {
$('#product_id').val(''); $('#product_id').val('');
$('#product_id_text_input').val(''); $('#product_id_text_input').val('');
@@ -69,6 +71,7 @@ $('#product_id').on('change', function(e)
$('#product_id_text_input').closest('.form-group').addClass('has-error'); $('#product_id_text_input').closest('.form-group').addClass('has-error');
$('#product-error').text('This product is not in stock.'); $('#product-error').text('This product is not in stock.');
$('#product-error').show(); $('#product-error').show();
$('#product_id_text_input').focus();
} }
else else
{ {
@@ -88,7 +91,22 @@ $('#product_id').on('change', function(e)
$(function() $(function()
{ {
$('.combobox').combobox({ appendId: '_text_input' }); $('.combobox').combobox({
appendId: '_text_input'
});
$('#product_id_text_input').on('change', function(e)
{
var input = $('#product_id_text_input').val().toString();
var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + input + "']").first();
if (possibleOptionElement.length > 0)
{
$('#product_id').val(possibleOptionElement.val());
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
}
});
$('#amount').val(1); $('#amount').val(1);
$('#product_id').val(''); $('#product_id').val('');
@@ -99,6 +117,14 @@ $(function()
$('#consumption-form').validator(); $('#consumption-form').validator();
$('#consumption-form').validator('validate'); $('#consumption-form').validator('validate');
$('#amount').on('focus', function(e)
{
if ($('#product_id_text_input').val().length === 0)
{
$('#product_id_text_input').focus();
}
});
$('#consumption-form input').keydown(function(event) $('#consumption-form input').keydown(function(event)
{ {
if (event.keyCode === 13) //Enter if (event.keyCode === 13) //Enter

View File

@@ -1,32 +1,39 @@
<div class="col-sm-3 col-sm-offset-3 col-md-3 col-md-offset-2 main"> <div class="col-sm-3 col-sm-offset-3 col-md-3 col-md-offset-2 main">
<h1 class="page-header">Consumption</h1> <h1 class="page-header">Consumption</h1>
<form id="consumption-form"> <form id="consumption-form">
<div class="form-group"> <div class="form-group">
<label for="product_id">Product&nbsp;&nbsp;<i class="fa fa-barcode"></i></label> <label for="product_id">Product&nbsp;&nbsp;<i class="fa fa-barcode"></i></label>
<select data-instockproduct="instockproduct" class="form-control combobox" id="product_id" name="product_id" required> <select class="form-control combobox" id="product_id" name="product_id" required>
<option value=""></option> <option value=""></option>
<?php foreach ($products as $product) : ?> <?php foreach ($products as $product) : ?>
<option value="<?php echo $product->id; ?>"><?php echo $product->name; ?><?php if (!empty($product->barcode)) echo ' [' . $product->barcode . ']'; ?></option> <option data-additional-searchdata="<?php echo $product->barcode; ?>" value="<?php echo $product->id; ?>"><?php echo $product->name; ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<div id="product-error" class="help-block with-errors"></div> <div id="product-error" class="help-block with-errors"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="amount">Amount</label> <label for="amount">Amount&nbsp;&nbsp;<span id="amount_qu_unit" class="small text-muted"></span></label>
<input type="number" class="form-control" id="amount" name="amount" value="1" min="1" required> <input type="number" class="form-control" id="amount" name="amount" value="1" min="1" required>
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<div class="checkbox"> <div class="checkbox">
<label for="spoiled"> <label for="spoiled">
<input type="checkbox" id="spoiled" name="spoiled"> Spoiled <input type="checkbox" id="spoiled" name="spoiled"> Spoiled
</label> </label>
</div> </div>
<button id="save-consumption-button" type="submit" class="btn btn-default">OK</button> <button id="save-consumption-button" type="submit" class="btn btn-default">OK</button>
</form> </form>
</div> </div>
<div class="col-sm-4 col-md-4 main well"> <div class="col-sm-6 col-md-5 col-lg-3 main well">
<h3>Product overview <strong><span id="selected-product-name"></span></strong></h3> <h3>Product overview <strong><span id="selected-product-name"></span></strong></h3>
<h4><strong>Stock quantity unit:</strong> <span id="selected-product-stock-qu-name"></span></h4> <h4><strong>Stock quantity unit:</strong> <span id="selected-product-stock-qu-name"></span></h4>

View File

@@ -1,7 +1,7 @@
$(function() $(function()
{ {
$('#current-stock-table').DataTable({ $('#current-stock-table').DataTable({
'paging': false, 'pageLength': 50,
'order': [[2, 'asc']] 'order': [[2, 'asc']]
}); });
}); });

View File

@@ -1,7 +1,19 @@
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main"> <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header">Dashboard</h1> <h1 class="page-header">Dashboard</h1>
<h3>Current stock</h3> <h3>Stock overview <span class="text-muded small"><strong><?php echo count($currentStock) ?></strong> products with <strong><?php echo GrocyPhpHelper::SumArrayValue($currentStock, 'amount'); ?></strong> units in stock</span></h3>
<div class="container-fluid">
<div class="row">
<p class="btn btn-lg btn-warning no-real-button"><strong><?php echo count(GrocyPhpHelper::FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime('+5 days')), '<')); ?></strong> products expiring within the next 5 days</p>
<p class="btn btn-lg btn-danger no-real-button"><strong><?php echo count(GrocyPhpHelper::FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime('-1 days')), '<')); ?></strong> products are already expired</p>
<p class="btn btn-lg btn-info no-real-button"><strong><?php echo count($missingProducts); ?></strong> products are below defined min. stock amount</p>
</div>
</div>
<div class="discrete-content-separator-2x"></div>
<div class="table-responsive"> <div class="table-responsive">
<table id="current-stock-table" class="table table-striped"> <table id="current-stock-table" class="table table-striped">
<thead> <thead>
@@ -13,7 +25,7 @@
</thead> </thead>
<tbody> <tbody>
<?php foreach ($currentStock as $currentStockEntry) : ?> <?php foreach ($currentStock as $currentStockEntry) : ?>
<tr> <tr class="<?php if ($currentStockEntry->best_before_date < date('Y-m-d', strtotime('-1 days'))) echo 'error-bg'; else if ($currentStockEntry->best_before_date < date('Y-m-d', strtotime('+5 days'))) echo 'warning-bg'; else if (GrocyPhpHelper::FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) echo 'info-bg'; ?>">
<td> <td>
<?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name; ?> <?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name; ?>
</td> </td>
@@ -21,11 +33,13 @@
<?php echo $currentStockEntry->amount; ?> <?php echo $currentStockEntry->amount; ?>
</td> </td>
<td> <td>
<?php echo $currentStockEntry->best_before_date; ?> <time class="timeago timeago-contextual" datetime="<?php echo $currentStockEntry->best_before_date; ?>"></time> <?php echo $currentStockEntry->best_before_date; ?>
<time class="timeago timeago-contextual" datetime="<?php echo $currentStockEntry->best_before_date; ?>"></time>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>

369
views/inventory.js Normal file
View File

@@ -0,0 +1,369 @@
$('#save-inventory-button').on('click', function(e)
{
e.preventDefault();
var jsonForm = $('#inventory-form').serializeJSON();
Grocy.FetchJson('/api/stock/get-product-details/' + jsonForm.product_id,
function (productDetails)
{
Grocy.FetchJson('/api/stock/inventory-product/' + jsonForm.product_id + '/' + jsonForm.new_amount + '?bestbeforedate=' + $('#best_before_date').val(),
function(result)
{
var addBarcode = Grocy.GetUriParam('addbarcodetoselection');
if (addBarcode !== undefined)
{
var existingBarcodes = productDetails.product.barcode || '';
if (existingBarcodes.length === 0)
{
productDetails.product.barcode = addBarcode;
}
else
{
productDetails.product.barcode += ',' + addBarcode;
}
Grocy.PostJson('/api/edit-object/products/' + productDetails.product.id, productDetails.product,
function (result) { },
function(xhr)
{
console.error(xhr);
}
);
}
toastr.success('Stock amount of ' + productDetails.product.name + ' is now ' + jsonForm.new_amount.toString() + ' ' + productDetails.quantity_unit_stock.name);
Grocy.Wait(1000);
if (addBarcode !== undefined)
{
window.location.href = '/inventory';
}
else
{
$('#inventory-change-info').hide();
$('#new_amount').val('');
$('#best_before_date').val('');
$('#product_id').val('');
$('#product_id_text_input').focus();
$('#product_id_text_input').val('');
$('#product_id_text_input').trigger('change');
$('#inventory-form').validator('validate');
}
},
function(xhr)
{
console.error(xhr);
}
);
},
function(xhr)
{
console.error(xhr);
}
);
});
$('#product_id').on('change', function(e)
{
var productId = $(e.target).val();
if (productId)
{
Grocy.FetchJson('/api/stock/get-product-details/' + productId,
function(productDetails)
{
$('#selected-product-name').text(productDetails.product.name);
$('#selected-product-stock-amount').text(productDetails.stock_amount || '0');
$('#selected-product-stock-qu-name').text(productDetails.quantity_unit_stock.name);
$('#selected-product-purchase-qu-name').text(productDetails.quantity_unit_purchase.name);
$('#selected-product-last-purchased').text((productDetails.last_purchased || 'never').substring(0, 10));
$('#selected-product-last-purchased-timeago').text($.timeago(productDetails.last_purchased || ''));
$('#selected-product-last-used').text((productDetails.last_used || 'never').substring(0, 10));
$('#selected-product-last-used-timeago').text($.timeago(productDetails.last_used || ''));
$('#new_amount').attr('not-equal', productDetails.stock_amount);
$('#new_amount_qu_unit').text(productDetails.quantity_unit_stock.name);
Grocy.EmptyElementWhenMatches('#selected-product-last-purchased-timeago', 'NaN years ago');
Grocy.EmptyElementWhenMatches('#selected-product-last-used-timeago', 'NaN years ago');
},
function(xhr)
{
console.error(xhr);
}
);
}
});
$(function()
{
$('.datepicker').datepicker(
{
format: 'yyyy-mm-dd',
startDate: '+0d',
todayHighlight: true,
autoclose: true,
calendarWeeks: true,
orientation: 'bottom auto',
weekStart: 1,
showOnFocus: false
});
$('.datepicker').trigger('change');
$('.combobox').combobox({
appendId: '_text_input'
});
$('#product_id_text_input').on('change', function(e)
{
var input = $('#product_id_text_input').val().toString();
var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + input + "']").first();
if (Grocy.GetUriParam('addbarcodetoselection') === undefined && possibleOptionElement.length > 0)
{
$('#product_id').val(possibleOptionElement.val());
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
}
else
{
var optionElement = $("#product_id option:contains('" + input + "')").first();
if (input.length > 0 && optionElement.length === 0 && Grocy.GetUriParam('addbarcodetoselection') === undefined )
{
bootbox.dialog({
message: '<strong>' + input + '</strong> could not be resolved to a product, how do you want to proceed?',
title: 'Create or assign product',
onEscape: function() { },
size: 'large',
backdrop: true,
buttons: {
cancel: {
label: 'Cancel',
className: 'btn-default',
callback: function() { }
},
addnewproduct: {
label: 'Add as new <u><strong>p</strong></u>roduct',
className: 'btn-success add-new-product-dialog-button',
callback: function()
{
window.location.href = '/product/new?prefillname=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname);
}
},
addbarcode: {
label: 'Add as <u><strong>b</strong></u>arcode to existing product',
className: 'btn-info add-new-barcode-dialog-button',
callback: function()
{
window.location.href = '/inventory?addbarcodetoselection=' + encodeURIComponent(input);
}
},
addnewproductwithbarcode: {
label: '<u><strong>A</strong></u>dd as new product + prefill barcode',
className: 'btn-warning add-new-product-with-barcode-dialog-button',
callback: function()
{
window.location.href = '/product/new?prefillbarcode=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname);
}
}
}
}).on('keypress', function(e)
{
if (e.key === 'B' || e.key === 'b')
{
$('.add-new-barcode-dialog-button').click();
}
if (e.key === 'p' || e.key === 'P')
{
$('.add-new-product-dialog-button').click();
}
if (e.key === 'a' || e.key === 'A')
{
$('.add-new-product-with-barcode-dialog-button').click();
}
});
}
}
});
$('#new_amount').val('');
$('#best_before_date').val('');
$('#product_id').val('');
$('#product_id_text_input').focus();
$('#product_id_text_input').val('');
$('#product_id_text_input').trigger('change');
$('#inventory-form').validator({
custom: {
'isodate': function($el)
{
if ($el.val().length !== 0 && !moment($el.val(), 'YYYY-MM-DD', true).isValid())
{
return 'Wrong date format, needs to be YYYY-MM-DD';
}
else if (moment($el.val()).isValid())
{
if (moment($el.val()).isBefore(moment(), 'day'))
{
return 'This value cannot be before today.';
}
}
},
'notequal': function($el)
{
if ($el.val().length !== 0 && $el.val().toString() === $el.attr('not-equal').toString())
{
return 'This value cannot be equal to ' + $el.attr('not-equal').toString();
}
}
}
});
$('#inventory-form').validator('validate');
$('#new_amount').on('focus', function(e)
{
if ($('#product_id_text_input').val().length === 0)
{
$('#product_id_text_input').focus();
}
});
$('#inventory-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
if ($('#inventory-form').validator('validate').has('.has-error').length !== 0) //There is at least one validation error
{
event.preventDefault();
return false;
}
}
});
var prefillProduct = Grocy.GetUriParam('createdproduct');
if (prefillProduct !== undefined)
{
var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + prefillProduct + "']").first();
if (possibleOptionElement.length === 0)
{
possibleOptionElement = $("#product_id option:contains('" + prefillProduct + "')").first();
}
if (possibleOptionElement.length > 0)
{
$('#product_id').val(possibleOptionElement.val());
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
$('#new_amount').focus();
}
}
var addBarcode = Grocy.GetUriParam('addbarcodetoselection');
if (addBarcode !== undefined)
{
$('#addbarcodetoselection').text(addBarcode);
$('#flow-info-addbarcodetoselection').removeClass('hide');
$('#barcode-lookup-disabled-hint').removeClass('hide');
}
});
$('#best_before_date-datepicker-button').on('click', function(e)
{
$('.datepicker').datepicker('show');
});
$('#best_before_date').on('change', function(e)
{
var value = $('#best_before_date').val();
if (value.length === 8 && $.isNumeric(value))
{
value = value.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3');
$('#best_before_date').val(value);
$('#inventory-form').validator('validate');
}
});
$('#best_before_date').on('keypress', function(e)
{
var element = $(e.target);
var value = element.val();
var dateObj = moment(element.val(), 'YYYY-MM-DD', true);
$('.datepicker').datepicker('hide');
//If input is empty and any arrow key is pressed, set date to today
if (value.length === 0 && (e.keyCode === 38 || e.keyCode === 40 || e.keyCode === 37 || e.keyCode === 39))
{
dateObj = moment(new Date(), 'YYYY-MM-DD', true);
}
if (dateObj.isValid())
{
if (e.keyCode === 38) //Up
{
element.val(dateObj.add(-1, 'days').format('YYYY-MM-DD'));
}
else if (e.keyCode === 40) //Down
{
element.val(dateObj.add(1, 'days').format('YYYY-MM-DD'));
}
else if (e.keyCode === 37) //Left
{
element.val(dateObj.add(-1, 'weeks').format('YYYY-MM-DD'));
}
else if (e.keyCode === 39) //Right
{
element.val(dateObj.add(1, 'weeks').format('YYYY-MM-DD'));
}
}
$('#inventory-form').validator('validate');
});
$('#new_amount').on('change', function(e)
{
if ($('#product_id').parent().hasClass('has-error'))
{
$('#inventory-change-info').hide();
return;
}
var productId = $('#product_id').val();
var newAmount = $('#new_amount').val();
if (productId)
{
Grocy.FetchJson('/api/stock/get-product-details/' + productId,
function(productDetails)
{
var productStockAmount = productDetails.stock_amount || '0';
if (newAmount > productStockAmount)
{
var amountToAdd = newAmount - productDetails.stock_amount;
$('#inventory-change-info').text('This means ' + amountToAdd.toString() + ' ' + productDetails.quantity_unit_stock.name + ' will be added to stock');
$('#inventory-change-info').show();
$('#best_before_date').attr('required', 'required');
}
else if (newAmount < productStockAmount)
{
var amountToRemove = productStockAmount - newAmount;
$('#inventory-change-info').text('This means ' + amountToRemove.toString() + ' ' + productDetails.quantity_unit_stock.name + ' will be removed from stock');
$('#inventory-change-info').show();
$('#best_before_date').removeAttr('required');
}
else
{
$('#inventory-change-info').hide();
}
$('#inventory-form').validator('update');
$('#inventory-form').validator('validate');
},
function(xhr)
{
console.error(xhr);
}
);
}
});

52
views/inventory.php Normal file
View File

@@ -0,0 +1,52 @@
<div class="col-sm-4 col-sm-offset-3 col-md-3 col-md-offset-2 main">
<h1 class="page-header">Inventory</h1>
<form id="inventory-form">
<div class="form-group">
<label for="product_id">Product&nbsp;&nbsp;<i class="fa fa-barcode"></i><span id="barcode-lookup-disabled-hint" class="small text-muted hide">&nbsp;&nbsp;Barcode lookup is disabled</span></label>
<select class="form-control combobox" id="product_id" name="product_id" required>
<option value=""></option>
<?php foreach ($products as $product) : ?>
<option data-additional-searchdata="<?php echo $product->barcode; ?>" value="<?php echo $product->id; ?>"><?php echo $product->name; ?></option>
<?php endforeach; ?>
</select>
<div class="help-block with-errors"></div>
<div id="flow-info-addbarcodetoselection" class="text-muted small hide"><strong><span id="addbarcodetoselection"></span></strong> will be added to the list of barcodes for the selected product on submit.</div>
</div>
<div class="form-group">
<label for="new_amount">New amount&nbsp;&nbsp;<span id="new_amount_qu_unit" class="small text-muted"></span></label>
<input type="number" data-notequal="notequal" class="form-control" id="new_amount" name="new_amount" min="0" not-equal="-1" required>
<div class="help-block with-errors"></div>
<div id="inventory-change-info" class="help-block text-muted"></div>
</div>
<div class="form-group">
<label for="best_before_date">Best before&nbsp;&nbsp;<span class="small text-muted">This will apply to added products</span></label>
<div class="input-group date">
<input type="text" data-isodate="isodate" class="form-control datepicker" id="best_before_date" name="best_before_date" autocomplete="off">
<div id="best_before_date-datepicker-button" class="input-group-addon">
<i class="fa fa-calendar"></i>
</div>
</div>
<div class="help-block with-errors"></div>
</div>
<button id="save-inventory-button" type="submit" class="btn btn-default">OK</button>
</form>
</div>
<div class="col-sm-6 col-md-5 col-lg-3 main well">
<h3>Product overview <strong><span id="selected-product-name"></span></strong></h3>
<h4><strong>Purchase quantity:</strong> <span id="selected-product-purchase-qu-name"></span></h4>
<p>
<strong>Stock amount:</strong> <span id="selected-product-stock-amount"></span> <span id="selected-product-stock-qu-name"></span><br />
<strong>Last purchased:</strong> <span id="selected-product-last-purchased"></span> <time id="selected-product-last-purchased-timeago" class="timeago timeago-contextual"></time><br />
<strong>Last used:</strong> <span id="selected-product-last-used"></span> <time id="selected-product-last-used-timeago" class="timeago timeago-contextual"></time>
</p>
</div>

View File

@@ -18,6 +18,7 @@
<link href="/bower_components/datatables.net-bs/css/dataTables.bootstrap.min.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" /> <link href="/bower_components/datatables.net-bs/css/dataTables.bootstrap.min.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" />
<link href="/bower_components/datatables.net-responsive-bs/css/responsive.bootstrap.min.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" /> <link href="/bower_components/datatables.net-responsive-bs/css/responsive.bootstrap.min.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" />
<link href="/bower_components/toastr/toastr.min.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" /> <link href="/bower_components/toastr/toastr.min.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" />
<link href="/bower_components/tagmanager/tagmanager.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" />
<link href="/style.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" /> <link href="/style.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" />
<script src="/bower_components/jquery/dist/jquery.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script> <script src="/bower_components/jquery/dist/jquery.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
@@ -37,6 +38,7 @@
</div> </div>
<div id="navbar-mobile" class="navbar-collapse collapse"> <div id="navbar-mobile" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li data-nav-for-page="dashboard.php"> <li data-nav-for-page="dashboard.php">
<a class="discrete-link" href="/"><i class="fa fa-tachometer fa-fw"></i>&nbsp;Dashboard</a> <a class="discrete-link" href="/"><i class="fa fa-tachometer fa-fw"></i>&nbsp;Dashboard</a>
@@ -47,7 +49,14 @@
<li data-nav-for-page="consumption.php"> <li data-nav-for-page="consumption.php">
<a class="discrete-link" href="/consumption"><i class="fa fa-cutlery fa-fw"></i>&nbsp;Record consumption</a> <a class="discrete-link" href="/consumption"><i class="fa fa-cutlery fa-fw"></i>&nbsp;Record consumption</a>
</li> </li>
<li data-nav-for-page="inventory.php">
<a class="discrete-link" href="/inventory"><i class="fa fa-list fa-fw"></i>&nbsp;Inventory</a>
</li>
<li data-nav-for-page="shoppinglist.php">
<a class="discrete-link" href="/shoppinglist"><i class="fa fa-shopping-bag fa-fw"></i>&nbsp;Shopping list</a>
</li>
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li data-nav-for-page="products.php"> <li data-nav-for-page="products.php">
<a class="discrete-link" href="/products"><i class="fa fa-product-hunt fa-fw"></i>&nbsp;Manage products</a> <a class="discrete-link" href="/products"><i class="fa fa-product-hunt fa-fw"></i>&nbsp;Manage products</a>
@@ -59,13 +68,16 @@
<a class="discrete-link" href="/quantityunits"><i class="fa fa-balance-scale fa-fw"></i>&nbsp;Manage quantity units</a> <a class="discrete-link" href="/quantityunits"><i class="fa fa-balance-scale fa-fw"></i>&nbsp;Manage quantity units</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</nav> </nav>
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-sm-3 col-md-2 sidebar"> <div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar"> <ul class="nav nav-sidebar">
<li data-nav-for-page="dashboard.php"> <li data-nav-for-page="dashboard.php">
<a class="discrete-link" href="/"><i class="fa fa-tachometer fa-fw"></i>&nbsp;Dashboard</a> <a class="discrete-link" href="/"><i class="fa fa-tachometer fa-fw"></i>&nbsp;Dashboard</a>
@@ -76,7 +88,14 @@
<li data-nav-for-page="consumption.php"> <li data-nav-for-page="consumption.php">
<a class="discrete-link" href="/consumption"><i class="fa fa-cutlery fa-fw"></i>&nbsp;Record consumption</a> <a class="discrete-link" href="/consumption"><i class="fa fa-cutlery fa-fw"></i>&nbsp;Record consumption</a>
</li> </li>
<li data-nav-for-page="inventory.php">
<a class="discrete-link" href="/inventory"><i class="fa fa-list fa-fw"></i>&nbsp;Inventory</a>
</li>
<li data-nav-for-page="shoppinglist.php">
<a class="discrete-link" href="/shoppinglist"><i class="fa fa-shopping-bag fa-fw"></i>&nbsp;Shopping list</a>
</li>
</ul> </ul>
<ul class="nav nav-sidebar"> <ul class="nav nav-sidebar">
<li data-nav-for-page="products.php"> <li data-nav-for-page="products.php">
<a class="discrete-link" href="/products"><i class="fa fa-product-hunt fa-fw"></i>&nbsp;Manage products</a> <a class="discrete-link" href="/products"><i class="fa fa-product-hunt fa-fw"></i>&nbsp;Manage products</a>
@@ -88,6 +107,7 @@
<a class="discrete-link" href="/quantityunits"><i class="fa fa-balance-scale fa-fw"></i>&nbsp;Manage quantity units</a> <a class="discrete-link" href="/quantityunits"><i class="fa fa-balance-scale fa-fw"></i>&nbsp;Manage quantity units</a>
</li> </li>
</ul> </ul>
<div class="nav-copyright nav nav-sidebar"> <div class="nav-copyright nav nav-sidebar">
grocy is a project by grocy is a project by
<a class="discrete-link" href="https://berrnd.de" target="_blank">Bernd Bestel</a> <a class="discrete-link" href="https://berrnd.de" target="_blank">Bernd Bestel</a>
@@ -102,10 +122,12 @@
<i class="fa fa-github"></i> <i class="fa fa-github"></i>
</a> </a>
</div> </div>
</div> </div>
<script>Grocy.ContentPage = '<?php echo $contentPage; ?>';</script> <script>Grocy.ContentPage = '<?php echo $contentPage; ?>';</script>
<?php include $contentPage; ?> <?php include $contentPage; ?>
</div> </div>
</div> </div>
@@ -122,6 +144,7 @@
<script src="/bower_components/datatables.net-responsive-bs/js/responsive.bootstrap.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script> <script src="/bower_components/datatables.net-responsive-bs/js/responsive.bootstrap.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
<script src="/bower_components/jquery-timeago/jquery.timeago.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script> <script src="/bower_components/jquery-timeago/jquery.timeago.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
<script src="/bower_components/toastr/toastr.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script> <script src="/bower_components/toastr/toastr.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
<script src="/bower_components/tagmanager/tagmanager.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
<?php if (file_exists(__DIR__ . '/' . str_replace('.php', '.js', $contentPage))) : ?> <?php if (file_exists(__DIR__ . '/' . str_replace('.php', '.js', $contentPage))) : ?>
<script src="/views/<?php echo str_replace('.php', '.js', $contentPage) . '?v=' . Grocy::GetInstalledVersion(); ?>"></script> <script src="/views/<?php echo str_replace('.php', '.js', $contentPage) . '?v=' . Grocy::GetInstalledVersion(); ?>"></script>

View File

@@ -1,4 +1,5 @@
<div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2 main"> <div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2 main">
<h1 class="page-header"><?php echo $title; ?></h1> <h1 class="page-header"><?php echo $title; ?></h1>
<script>Grocy.EditMode = '<?php echo $mode; ?>';</script> <script>Grocy.EditMode = '<?php echo $mode; ?>';</script>
@@ -8,15 +9,20 @@
<?php endif; ?> <?php endif; ?>
<form id="location-form"> <form id="location-form">
<div class="form-group"> <div class="form-group">
<label for="name">Name</label> <label for="name">Name</label>
<input type="text" class="form-control" required id="name" name="name" value="<?php if ($mode == 'edit') echo $location->name; ?>" /> <input type="text" class="form-control" required id="name" name="name" value="<?php if ($mode == 'edit') echo $location->name; ?>" />
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="description">Description</label> <label for="description">Description</label>
<textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $location->description; ?></textarea> <textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $location->description; ?></textarea>
</div> </div>
<button id="save-location-button" type="submit" class="btn btn-default">Save</button> <button id="save-location-button" type="submit" class="btn btn-default">Save</button>
</form> </form>
</div> </div>

View File

@@ -14,7 +14,7 @@
}, },
callback: function(result) callback: function(result)
{ {
if (result == true) if (result === true)
{ {
Grocy.FetchJson('/api/delete-object/locations/' + $(e.target).attr('data-location-id'), Grocy.FetchJson('/api/delete-object/locations/' + $(e.target).attr('data-location-id'),
function(result) function(result)
@@ -41,4 +41,3 @@ $(function()
] ]
}); });
}); });

View File

@@ -1,4 +1,5 @@
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main"> <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header"> <h1 class="page-header">
Locations Locations
<a class="btn btn-default" href="/location/new" role="button"> <a class="btn btn-default" href="/location/new" role="button">
@@ -37,4 +38,5 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>

View File

@@ -2,12 +2,19 @@
{ {
e.preventDefault(); e.preventDefault();
var redirectDestination = '/products';
var returnTo = Grocy.GetUriParam('returnto');
if (returnTo !== undefined)
{
redirectDestination = returnTo + '?createdproduct=' + encodeURIComponent($('#name').val());
}
if (Grocy.EditMode === 'create') if (Grocy.EditMode === 'create')
{ {
Grocy.PostJson('/api/add-object/products', $('#product-form').serializeJSON(), Grocy.PostJson('/api/add-object/products', $('#product-form').serializeJSON(),
function(result) function(result)
{ {
window.location.href = '/products'; window.location.href = redirectDestination;
}, },
function(xhr) function(xhr)
{ {
@@ -20,7 +27,7 @@
Grocy.PostJson('/api/edit-object/products/' + Grocy.EditObjectId, $('#product-form').serializeJSON(), Grocy.PostJson('/api/edit-object/products/' + Grocy.EditObjectId, $('#product-form').serializeJSON(),
function(result) function(result)
{ {
window.location.href = '/products'; window.location.href = redirectDestination;
}, },
function(xhr) function(xhr)
{ {
@@ -32,16 +39,61 @@
$(function() $(function()
{ {
$('#barcode-taginput').tagsManager({
'hiddenTagListName': 'barcode',
'tagsContainer': '#barcode-taginput-container'
});
if (Grocy.EditMode === 'edit')
{
Grocy.FetchJson('/api/get-object/products/' + Grocy.EditObjectId,
function (product)
{
if (product.barcode !== null && product.barcode.length > 0)
{
product.barcode.split(',').forEach(function(item)
{
$('#barcode-taginput').tagsManager('pushTag', item);
});
}
},
function(xhr)
{
console.error(xhr);
}
);
}
$('#qu_factor_purchase_to_stock').trigger('change');
$('#name').focus(); $('#name').focus();
$('#product-form').validator(); $('#product-form').validator();
$('#product-form').validator('validate'); $('#product-form').validator('validate');
});
$('#barcode').keydown(function(event) var prefillName = Grocy.GetUriParam('prefillname');
if (prefillName !== undefined)
{ {
if (event.keyCode === 13) //Enter $('#name').val(prefillName);
$('#name').focus();
}
var prefillBarcode = Grocy.GetUriParam('prefillbarcode');
if (prefillBarcode !== undefined)
{ {
event.preventDefault(); $('#barcode-taginput').tagsManager('pushTag', prefillBarcode);
return false; $('#name').focus();
}
});
$('.input-group-qu').on('change', function(e)
{
var factor = $('#qu_factor_purchase_to_stock').val();
if (factor > 1)
{
$('#qu-conversion-info').text('This means 1 ' + $("#qu_id_purchase option:selected").text() + ' purchased will be converted into ' + (1 * factor).toString() + ' ' + $("#qu_id_stock option:selected").text() + ' in stock.');
$('#qu-conversion-info').show();
}
else
{
$('#qu-conversion-info').hide();
} }
}); });

View File

@@ -1,4 +1,5 @@
<div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2 main"> <div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2 main">
<h1 class="page-header"><?php echo $title; ?></h1> <h1 class="page-header"><?php echo $title; ?></h1>
<script>Grocy.EditMode = '<?php echo $mode; ?>';</script> <script>Grocy.EditMode = '<?php echo $mode; ?>';</script>
@@ -8,24 +9,24 @@
<?php endif; ?> <?php endif; ?>
<form id="product-form"> <form id="product-form">
<div class="form-group"> <div class="form-group">
<label for="name">Name</label> <label for="name">Name</label>
<input type="text" class="form-control" required id="name" name="name" value="<?php if ($mode == 'edit') echo $product->name; ?>"> <input type="text" class="form-control" required id="name" name="name" value="<?php if ($mode == 'edit') echo $product->name; ?>">
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<div class="form-group">
<label for="barcode">Barcode</label>
<div class="input-group date">
<input type="text" class="form-control" id="barcode" name="barcode" value="<?php if ($mode == 'edit') echo $product->barcode; ?>">
<div class="input-group-addon">
<i class="fa fa-barcode"></i>
</div>
</div>
</div>
<div class="form-group"> <div class="form-group">
<label for="description">Description</label> <label for="description">Description</label>
<textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $product->description; ?></textarea> <textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $product->description; ?></textarea>
</div> </div>
<div class="form-group tm-group">
<label for="barcode-taginput">Barcode(s)&nbsp;&nbsp;<i class="fa fa-barcode"></i></label>
<input type="text" class="form-control tm-input" id="barcode-taginput">
<div id="barcode-taginput-container"></div>
</div>
<div class="form-group"> <div class="form-group">
<label for="location_id">Location</label> <label for="location_id">Location</label>
<select required class="form-control" id="location_id" name="location_id"> <select required class="form-control" id="location_id" name="location_id">
@@ -35,29 +36,48 @@
</select> </select>
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<div class="form-group">
<label for="min_stock_amount">Minimum stock amount</label>
<input required min="0" type="number" class="form-control" id="min_stock_amount" name="min_stock_amount" value="<?php if ($mode == 'edit') echo $product->min_stock_amount; else echo '0'; ?>">
<div class="help-block with-errors"></div>
</div>
<div class="form-group">
<label for="default_best_before_days">Default best before days<br /><span class="small text-muted">For purchases this amount of days will be added to today for the best before date suggestion</span></label>
<input required min="0" type="number" class="form-control" id="default_best_before_days" name="default_best_before_days" value="<?php if ($mode == 'edit') echo $product->default_best_before_days; else echo '0'; ?>">
<div class="help-block with-errors"></div>
</div>
<div class="form-group"> <div class="form-group">
<label for="qu_id_purchase">Quantity unit purchase</label> <label for="qu_id_purchase">Quantity unit purchase</label>
<select required class="form-control" id="qu_id_purchase" name="qu_id_purchase"> <select required class="form-control input-group-qu" id="qu_id_purchase" name="qu_id_purchase">
<?php foreach ($quantityunits as $quantityunit) : ?> <?php foreach ($quantityunits as $quantityunit) : ?>
<option <?php if ($mode == 'edit' && $quantityunit->id == $product->qu_id_purchase) echo 'selected="selected"'; ?> value="<?php echo $quantityunit->id; ?>"><?php echo $quantityunit->name; ?></option> <option <?php if ($mode == 'edit' && $quantityunit->id == $product->qu_id_purchase) echo 'selected="selected"'; ?> value="<?php echo $quantityunit->id; ?>"><?php echo $quantityunit->name; ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="qu_id_stock">Quantity unit stock</label> <label for="qu_id_stock">Quantity unit stock</label>
<select required class="form-control" id="qu_id_stock" name="qu_id_stock"> <select required class="form-control input-group-qu" id="qu_id_stock" name="qu_id_stock">
<?php foreach ($quantityunits as $quantityunit) : ?> <?php foreach ($quantityunits as $quantityunit) : ?>
<option <?php if ($mode == 'edit' && $quantityunit->id == $product->qu_id_stock) echo 'selected="selected"'; ?> value="<?php echo $quantityunit->id; ?>"><?php echo $quantityunit->name; ?></option> <option <?php if ($mode == 'edit' && $quantityunit->id == $product->qu_id_stock) echo 'selected="selected"'; ?> value="<?php echo $quantityunit->id; ?>"><?php echo $quantityunit->name; ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="qu_factor_purchase_to_stock">Factor purchase to stock quantity unit</label> <label for="qu_factor_purchase_to_stock">Factor purchase to stock quantity unit</label>
<input required min="1" type="number" class="form-control" id="qu_factor_purchase_to_stock" name="qu_factor_purchase_to_stock" value="<?php if ($mode == 'edit') echo $product->qu_factor_purchase_to_stock; else echo '1'; ?>"> <input required min="1" type="number" class="form-control input-group-qu" id="qu_factor_purchase_to_stock" name="qu_factor_purchase_to_stock" value="<?php if ($mode == 'edit') echo $product->qu_factor_purchase_to_stock; else echo '1'; ?>">
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<p id="qu-conversion-info" class="help-block text-muted"></p>
<button id="save-product-button" type="submit" class="btn btn-default">Save</button> <button id="save-product-button" type="submit" class="btn btn-default">Save</button>
</form> </form>
</div> </div>

View File

@@ -14,7 +14,7 @@
}, },
callback: function(result) callback: function(result)
{ {
if (result == true) if (result === true)
{ {
Grocy.FetchJson('/api/delete-object/products/' + $(e.target).attr('data-product-id'), Grocy.FetchJson('/api/delete-object/products/' + $(e.target).attr('data-product-id'),
function(result) function(result)

View File

@@ -1,4 +1,5 @@
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main"> <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header"> <h1 class="page-header">
Products Products
<a class="btn btn-default" href="/product/new" role="button"> <a class="btn btn-default" href="/product/new" role="button">
@@ -13,6 +14,7 @@
<th>#</th> <th>#</th>
<th>Name</th> <th>Name</th>
<th>Location</th> <th>Location</th>
<th>Min. stock amount</th>
<th>QU purchase</th> <th>QU purchase</th>
<th>QU stock</th> <th>QU stock</th>
<th>QU factor</th> <th>QU factor</th>
@@ -36,6 +38,9 @@
<td> <td>
<?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($locations, 'id', $product->location_id)->name; ?> <?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($locations, 'id', $product->location_id)->name; ?>
</td> </td>
<td>
<?php echo $product->min_stock_amount; ?>
</td>
<td> <td>
<?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($quantityunits, 'id', $product->qu_id_purchase)->name; ?> <?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($quantityunits, 'id', $product->qu_id_purchase)->name; ?>
</td> </td>
@@ -53,4 +58,5 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>

View File

@@ -7,18 +7,42 @@
Grocy.FetchJson('/api/stock/get-product-details/' + jsonForm.product_id, Grocy.FetchJson('/api/stock/get-product-details/' + jsonForm.product_id,
function (productDetails) function (productDetails)
{ {
jsonForm.amount = jsonForm.amount * productDetails.product.qu_factor_purchase_to_stock; var amount = jsonForm.amount * productDetails.product.qu_factor_purchase_to_stock;
Grocy.FetchJson('/api/helper/uniqid', Grocy.FetchJson('/api/stock/add-product/' + jsonForm.product_id + '/' + amount + '?bestbeforedate=' + $('#best_before_date').val(),
function(uniqidResponse)
{
jsonForm.stock_id = uniqidResponse.uniqid;
Grocy.PostJson('/api/add-object/stock', jsonForm,
function(result) function(result)
{ {
toastr.success('Added ' + jsonForm.amount + ' ' + productDetails.quantity_unit_stock.name + ' of ' + productDetails.product.name + ' to stock'); var addBarcode = Grocy.GetUriParam('addbarcodetoselection');
if (addBarcode !== undefined)
{
var existingBarcodes = productDetails.product.barcode || '';
if (existingBarcodes.length === 0)
{
productDetails.product.barcode = addBarcode;
}
else
{
productDetails.product.barcode += ',' + addBarcode;
}
Grocy.PostJson('/api/edit-object/products/' + productDetails.product.id, productDetails.product,
function (result) { },
function(xhr)
{
console.error(xhr);
}
);
}
toastr.success('Added ' + amount + ' ' + productDetails.quantity_unit_stock.name + ' of ' + productDetails.product.name + ' to stock');
Grocy.Wait(1000);
if (addBarcode !== undefined)
{
window.location.href = '/purchase';
}
else
{
$('#amount').val(1); $('#amount').val(1);
$('#best_before_date').val(''); $('#best_before_date').val('');
$('#product_id').val(''); $('#product_id').val('');
@@ -26,12 +50,7 @@
$('#product_id_text_input').val(''); $('#product_id_text_input').val('');
$('#product_id_text_input').trigger('change'); $('#product_id_text_input').trigger('change');
$('#purchase-form').validator('validate'); $('#purchase-form').validator('validate');
},
function(xhr)
{
console.error(xhr);
} }
);
}, },
function(xhr) function(xhr)
{ {
@@ -63,6 +82,12 @@ $('#product_id').on('change', function(e)
$('#selected-product-last-purchased-timeago').text($.timeago(productDetails.last_purchased || '')); $('#selected-product-last-purchased-timeago').text($.timeago(productDetails.last_purchased || ''));
$('#selected-product-last-used').text((productDetails.last_used || 'never').substring(0, 10)); $('#selected-product-last-used').text((productDetails.last_used || 'never').substring(0, 10));
$('#selected-product-last-used-timeago').text($.timeago(productDetails.last_used || '')); $('#selected-product-last-used-timeago').text($.timeago(productDetails.last_used || ''));
$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name);
if (productDetails.product.default_best_before_days.toString() !== '0')
{
$('#best_before_date').val(moment().add(productDetails.product.default_best_before_days, 'days').format('YYYY-MM-DD'));
}
Grocy.EmptyElementWhenMatches('#selected-product-last-purchased-timeago', 'NaN years ago'); Grocy.EmptyElementWhenMatches('#selected-product-last-purchased-timeago', 'NaN years ago');
Grocy.EmptyElementWhenMatches('#selected-product-last-used-timeago', 'NaN years ago'); Grocy.EmptyElementWhenMatches('#selected-product-last-used-timeago', 'NaN years ago');
@@ -90,7 +115,81 @@ $(function()
}); });
$('.datepicker').trigger('change'); $('.datepicker').trigger('change');
$('.combobox').combobox({ appendId: '_text_input' }); $('.combobox').combobox({
appendId: '_text_input'
});
$('#product_id_text_input').on('change', function(e)
{
var input = $('#product_id_text_input').val().toString();
var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + input + "']").first();
if (Grocy.GetUriParam('addbarcodetoselection') === undefined && possibleOptionElement.length > 0)
{
$('#product_id').val(possibleOptionElement.val());
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
}
else
{
var optionElement = $("#product_id option:contains('" + input + "')").first();
if (input.length > 0 && optionElement.length === 0 && Grocy.GetUriParam('addbarcodetoselection') === undefined )
{
bootbox.dialog({
message: '<strong>' + input + '</strong> could not be resolved to a product, how do you want to proceed?',
title: 'Create or assign product',
onEscape: function() { },
size: 'large',
backdrop: true,
buttons: {
cancel: {
label: 'Cancel',
className: 'btn-default',
callback: function() { }
},
addnewproduct: {
label: 'Add as new <u><strong>p</strong></u>roduct',
className: 'btn-success add-new-product-dialog-button',
callback: function()
{
window.location.href = '/product/new?prefillname=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname);
}
},
addbarcode: {
label: 'Add as <u><strong>b</strong></u>arcode to existing product',
className: 'btn-info add-new-barcode-dialog-button',
callback: function()
{
window.location.href = '/purchase?addbarcodetoselection=' + encodeURIComponent(input);
}
},
addnewproductwithbarcode: {
label: '<u><strong>A</strong></u>dd as new product + prefill barcode',
className: 'btn-warning add-new-product-with-barcode-dialog-button',
callback: function()
{
window.location.href = '/product/new?prefillbarcode=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname);
}
}
}
}).on('keypress', function(e)
{
if (e.key === 'B' || e.key === 'b')
{
$('.add-new-barcode-dialog-button').click();
}
if (e.key === 'p' || e.key === 'P')
{
$('.add-new-product-dialog-button').click();
}
if (e.key === 'a' || e.key === 'A')
{
$('.add-new-product-with-barcode-dialog-button').click();
}
});
}
}
});
$('#amount').val(1); $('#amount').val(1);
$('#best_before_date').val(''); $('#best_before_date').val('');
@@ -107,11 +206,26 @@ $(function()
{ {
return 'Wrong date format, needs to be YYYY-MM-DD'; return 'Wrong date format, needs to be YYYY-MM-DD';
} }
else if (moment($el.val()).isValid())
{
if (moment($el.val()).isBefore(moment(), 'day'))
{
return 'This value cannot be before today.';
}
}
} }
} }
}); });
$('#purchase-form').validator('validate'); $('#purchase-form').validator('validate');
$('#best_before_date').on('focus', function(e)
{
if ($('#product_id_text_input').val().length === 0)
{
$('#product_id_text_input').focus();
}
});
$('#purchase-form input').keydown(function(event) $('#purchase-form input').keydown(function(event)
{ {
if (event.keyCode === 13) //Enter if (event.keyCode === 13) //Enter
@@ -123,6 +237,32 @@ $(function()
} }
} }
}); });
var prefillProduct = Grocy.GetUriParam('createdproduct');
if (prefillProduct !== undefined)
{
var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + prefillProduct + "']").first();
if (possibleOptionElement.length === 0)
{
possibleOptionElement = $("#product_id option:contains('" + prefillProduct + "')").first();
}
if (possibleOptionElement.length > 0)
{
$('#product_id').val(possibleOptionElement.val());
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
$('#best_before_date').focus();
}
}
var addBarcode = Grocy.GetUriParam('addbarcodetoselection');
if (addBarcode !== undefined)
{
$('#addbarcodetoselection').text(addBarcode);
$('#flow-info-addbarcodetoselection').removeClass('hide');
$('#barcode-lookup-disabled-hint').removeClass('hide');
}
}); });
$('#best_before_date-datepicker-button').on('click', function(e) $('#best_before_date-datepicker-button').on('click', function(e)
@@ -149,11 +289,13 @@ $('#best_before_date').on('keypress', function(e)
$('.datepicker').datepicker('hide'); $('.datepicker').datepicker('hide');
if (value.length === 0) //If input is empty and any arrow key is pressed, set date to today
if (value.length === 0 && (e.keyCode === 38 || e.keyCode === 40 || e.keyCode === 37 || e.keyCode === 39))
{ {
element.val(moment().format('YYYY-MM-DD')); dateObj = moment(new Date(), 'YYYY-MM-DD', true);
} }
else if (dateObj.isValid())
if (dateObj.isValid())
{ {
if (e.keyCode === 38) //Up if (e.keyCode === 38) //Up
{ {

View File

@@ -1,17 +1,21 @@
<div class="col-sm-4 col-sm-offset-3 col-md-3 col-md-offset-2 main"> <div class="col-sm-4 col-sm-offset-3 col-md-3 col-md-offset-2 main">
<h1 class="page-header">Purchase</h1> <h1 class="page-header">Purchase</h1>
<form id="purchase-form"> <form id="purchase-form">
<div class="form-group"> <div class="form-group">
<label for="product_id">Product&nbsp;&nbsp;<i class="fa fa-barcode"></i></label> <label for="product_id">Product&nbsp;&nbsp;<i class="fa fa-barcode"></i><span id="barcode-lookup-disabled-hint" class="small text-muted hide">&nbsp;&nbsp;Barcode lookup is disabled</span></label>
<select class="form-control combobox" id="product_id" name="product_id" required> <select class="form-control combobox" id="product_id" name="product_id" required>
<option value=""></option> <option value=""></option>
<?php foreach ($products as $product) : ?> <?php foreach ($products as $product) : ?>
<option value="<?php echo $product->id; ?>"><?php echo $product->name; ?><?php if (!empty($product->barcode)) echo ' [' . $product->barcode . ']'; ?></option> <option data-additional-searchdata="<?php echo $product->barcode; ?>" value="<?php echo $product->id; ?>"><?php echo $product->name; ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
<div id="flow-info-addbarcodetoselection" class="text-muted small hide"><strong><span id="addbarcodetoselection"></span></strong> will be added to the list of barcodes for the selected product on submit.</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="best_before_date">Best before</label> <label for="best_before_date">Best before</label>
<div class="input-group date"> <div class="input-group date">
@@ -22,16 +26,20 @@
</div> </div>
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="amount">Amount</label> <label for="amount">Amount&nbsp;&nbsp;<span id="amount_qu_unit" class="small text-muted"></span></label>
<input type="number" class="form-control" id="amount" name="amount" value="1" min="1" required> <input type="number" class="form-control" id="amount" name="amount" value="1" min="1" required>
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<button id="save-purchase-button" type="submit" class="btn btn-default">OK</button> <button id="save-purchase-button" type="submit" class="btn btn-default">OK</button>
</form> </form>
</div> </div>
<div class="col-sm-4 col-md-4 main well"> <div class="col-sm-6 col-md-5 col-lg-3 main well">
<h3>Product overview <strong><span id="selected-product-name"></span></strong></h3> <h3>Product overview <strong><span id="selected-product-name"></span></strong></h3>
<h4><strong>Purchase quantity:</strong> <span id="selected-product-purchase-qu-name"></span></h4> <h4><strong>Purchase quantity:</strong> <span id="selected-product-purchase-qu-name"></span></h4>

View File

@@ -1,4 +1,5 @@
<div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2 main"> <div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2 main">
<h1 class="page-header"><?php echo $title; ?></h1> <h1 class="page-header"><?php echo $title; ?></h1>
<script>Grocy.EditMode = '<?php echo $mode; ?>';</script> <script>Grocy.EditMode = '<?php echo $mode; ?>';</script>
@@ -8,15 +9,20 @@
<?php endif; ?> <?php endif; ?>
<form id="quantityunit-form"> <form id="quantityunit-form">
<div class="form-group"> <div class="form-group">
<label for="name">Name</label> <label for="name">Name</label>
<input type="text" class="form-control" required id="name" name="name" value="<?php if ($mode == 'edit') echo $quantityunit->name; ?>" /> <input type="text" class="form-control" required id="name" name="name" value="<?php if ($mode == 'edit') echo $quantityunit->name; ?>" />
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="description">Description</label> <label for="description">Description</label>
<textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $quantityunit->description; ?></textarea> <textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $quantityunit->description; ?></textarea>
</div> </div>
<button id="save-quantityunit-button" type="submit" class="btn btn-default">Save</button> <button id="save-quantityunit-button" type="submit" class="btn btn-default">Save</button>
</form> </form>
</div> </div>

View File

@@ -14,7 +14,7 @@
}, },
callback: function(result) callback: function(result)
{ {
if (result == true) if (result === true)
{ {
Grocy.FetchJson('/api/delete-object/quantity_units/' + $(e.target).attr('data-quantityunit-id'), Grocy.FetchJson('/api/delete-object/quantity_units/' + $(e.target).attr('data-quantityunit-id'),
function(result) function(result)

View File

@@ -1,4 +1,5 @@
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main"> <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header"> <h1 class="page-header">
Quantity units Quantity units
<a class="btn btn-default" href="/quantityunit/new" role="button"> <a class="btn btn-default" href="/quantityunit/new" role="button">
@@ -37,4 +38,5 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>

38
views/shoppinglist.js Normal file
View File

@@ -0,0 +1,38 @@
$(document).on('click', '.shoppinglist-delete-button', function(e)
{
Grocy.FetchJson('/api/delete-object/shopping_list/' + $(e.target).attr('data-shoppinglist-id'),
function(result)
{
window.location.href = '/shoppinglist';
},
function(xhr)
{
console.error(xhr);
}
);
});
$(document).on('click', '#add-products-below-min-stock-amount', function(e)
{
Grocy.FetchJson('/api/stock/add-missing-products-to-shoppinglist',
function(result)
{
window.location.href = '/shoppinglist';
},
function(xhr)
{
console.error(xhr);
}
);
});
$(function()
{
$('#shoppinglist-table').DataTable({
'pageLength': 50,
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
]
});
});

45
views/shoppinglist.php Normal file
View File

@@ -0,0 +1,45 @@
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header">
Shopping List
<a class="btn btn-default" href="/shoppinglist/new" role="button">
<i class="fa fa-plus"></i>&nbsp;Add
</a>
<a id="add-products-below-min-stock-amount" class="btn btn-info" href="#" role="button">
<i class="fa fa-plus"></i>&nbsp;Add products that are below defined min. stock amount
</a>
</h1>
<div class="table-responsive">
<table id="shoppinglist-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Product</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<?php foreach ($listItems as $listItem) : ?>
<tr>
<td class="fit-content">
<a class="btn btn-info" href="/shoppinglist/<?php echo $listItem->id; ?>" role="button">
<i class="fa fa-pencil"></i>
</a>
<a class="btn btn-danger shoppinglist-delete-button" href="#" role="button" data-shoppinglist-id="<?php echo $listItem->id; ?>">
<i class="fa fa-trash"></i>
</a>
</td>
<td>
<?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->name; ?>
</td>
<td>
<?php echo $listItem->amount + $listItem->amount_autoadded . ' ' . GrocyPhpHelper::FindObjectInArrayByPropertyValue($quantityunits, 'id', GrocyPhpHelper::FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->qu_id_purchase)->name; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>

147
views/shoppinglistform.js Normal file
View File

@@ -0,0 +1,147 @@
$('#save-shoppinglist-button').on('click', function(e)
{
e.preventDefault();
if (Grocy.EditMode === 'create')
{
Grocy.PostJson('/api/add-object/shopping_list', $('#shoppinglist-form').serializeJSON(),
function(result)
{
window.location.href = '/shoppinglist';
},
function(xhr)
{
console.error(xhr);
}
);
}
else
{
Grocy.PostJson('/api/edit-object/shopping_list/' + Grocy.EditObjectId, $('#shoppinglist-form').serializeJSON(),
function(result)
{
window.location.href = '/shoppinglist';
},
function(xhr)
{
console.error(xhr);
}
);
}
});
$('#product_id').on('change', function(e)
{
var productId = $(e.target).val();
if (productId)
{
Grocy.FetchJson('/api/stock/get-product-details/' + productId,
function (productDetails)
{
$('#selected-product-name').text(productDetails.product.name);
$('#selected-product-stock-amount').text(productDetails.stock_amount || '0');
$('#selected-product-stock-qu-name').text(productDetails.quantity_unit_stock.name);
$('#selected-product-stock-qu-name2').text(productDetails.quantity_unit_stock.name);
$('#selected-product-last-purchased').text((productDetails.last_purchased || 'never').substring(0, 10));
$('#selected-product-last-purchased-timeago').text($.timeago(productDetails.last_purchased || ''));
$('#selected-product-last-used').text((productDetails.last_used || 'never').substring(0, 10));
$('#selected-product-last-used-timeago').text($.timeago(productDetails.last_used || ''));
$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name);
Grocy.EmptyElementWhenMatches('#selected-product-last-purchased-timeago', 'NaN years ago');
Grocy.EmptyElementWhenMatches('#selected-product-last-used-timeago', 'NaN years ago');
if ($('#product_id').hasClass('suppress-next-custom-validate-event'))
{
$('#product_id').removeClass('suppress-next-custom-validate-event');
}
else
{
Grocy.FetchJson('/api/get-objects/shopping_list',
function (currentShoppingListItems)
{
if (currentShoppingListItems.filter(function (e) { return e.product_id === productId; }).length > 0)
{
$('#product_id').val('');
$('#product_id_text_input').val('');
$('#product_id_text_input').addClass('has-error');
$('#product_id_text_input').parent('.input-group').addClass('has-error');
$('#product_id_text_input').closest('.form-group').addClass('has-error');
$('#product-error').text('This product is already on the shopping list.');
$('#product-error').show();
$('#product_id_text_input').focus();
}
else
{
$('#product_id_text_input').removeClass('has-error');
$('#product_id_text_input').parent('.input-group').removeClass('has-error');
$('#product_id_text_input').closest('.form-group').removeClass('has-error');
$('#product-error').hide();
}
},
function(xhr)
{
console.error(xhr);
}
);
}
},
function(xhr)
{
console.error(xhr);
}
);
}
});
$(function()
{
$('.combobox').combobox({
appendId: '_text_input'
});
$('#product_id_text_input').on('change', function(e)
{
var input = $('#product_id_text_input').val().toString();
var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + input + "']").first();
if (possibleOptionElement.length > 0 && possibleOptionElement.text().length > 0) {
$('#product_id').val(possibleOptionElement.val());
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
}
});
$('#product_id_text_input').focus();
$('#product_id_text_input').trigger('change');
if (Grocy.EditMode === 'edit')
{
$('#product_id').addClass('suppress-next-custom-validate-event');
$('#product_id').trigger('change');
}
$('#shoppinglist-form').validator();
$('#shoppinglist-form').validator('validate');
$('#amount').on('focus', function(e)
{
if ($('#product_id_text_input').val().length === 0)
{
$('#product_id_text_input').focus();
}
});
$('#shoppinglist-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
if ($('#shoppinglist-form').validator('validate').has('.has-error').length !== 0) //There is at least one validation error
{
event.preventDefault();
return false;
}
}
});
});

View File

@@ -0,0 +1,45 @@
<div class="col-sm-3 col-sm-offset-3 col-md-3 col-md-offset-2 main">
<h1 class="page-header"><?php echo $title; ?></h1>
<script>Grocy.EditMode = '<?php echo $mode; ?>';</script>
<?php if ($mode == 'edit') : ?>
<script>Grocy.EditObjectId = <?php echo $listItem->id; ?>;</script>
<?php endif; ?>
<form id="shoppinglist-form">
<div class="form-group">
<label for="product_id">Product&nbsp;&nbsp;<i class="fa fa-barcode"></i></label>
<select class="form-control combobox" id="product_id" name="product_id" value="<?php if ($mode == 'edit') echo $listItem->product_id; ?>" required>
<option value=""></option>
<?php foreach ($products as $product) : ?>
<option <?php if ($mode == 'edit' && $product->id == $listItem->product_id) echo 'selected="selected"'; ?> data-additional-searchdata="<?php echo $product->barcode; ?>" value="<?php echo $product->id; ?>"><?php echo $product->name; ?></option>
<?php endforeach; ?>
</select>
<div id="product-error" class="help-block with-errors"></div>
</div>
<div class="form-group">
<label for="amount">Amount&nbsp;&nbsp;<span id="amount_qu_unit" class="small text-muted"></span></label>
<input type="number" class="form-control" id="amount" name="amount" value="<?php if ($mode == 'edit') echo $listItem->amount; else echo '1'; ?>" min="1" required>
<div class="help-block with-errors"></div>
</div>
<button id="save-shoppinglist-button" type="submit" class="btn btn-default">Save</button>
</form>
</div>
<div class="col-sm-6 col-md-5 col-lg-3 main well">
<h3>Product overview <strong><span id="selected-product-name"></span></strong></h3>
<h4><strong>Stock quantity unit:</strong> <span id="selected-product-stock-qu-name"></span></h4>
<p>
<strong>Stock amount:</strong> <span id="selected-product-stock-amount"></span> <span id="selected-product-stock-qu-name2"></span><br />
<strong>Last purchased:</strong> <span id="selected-product-last-purchased"></span> <time id="selected-product-last-purchased-timeago" class="timeago timeago-contextual"></time><br />
<strong>Last used:</strong> <span id="selected-product-last-used"></span> <time id="selected-product-last-used-timeago" class="timeago timeago-contextual"></time>
</p>
</div>