mirror of
https://github.com/grocy/grocy.git
synced 2025-09-16 17:56:51 +00:00
Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
96209c852c | ||
|
8e40c50cc1 | ||
|
4b0f0141c9 | ||
|
d1bd21a601 | ||
|
c6925ba4c3 | ||
|
52e311d847 | ||
|
f2f18d260d | ||
|
1d293741ba | ||
|
5db288fc3c | ||
|
d628f9b3ca | ||
|
fe8a6d96e4 | ||
|
bd16b8c851 | ||
|
c4a22c18f7 | ||
|
e38c24f9ed | ||
|
83a7534a74 | ||
|
dc530c80aa | ||
|
6aaaa2073b | ||
|
4f0dc818f6 | ||
|
3ed6c9b28a | ||
|
2b8c672279 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -198,6 +198,5 @@ FakesAssemblies/
|
||||
/bower_components
|
||||
/vendor
|
||||
/.release
|
||||
/config.php
|
||||
/composer.phar
|
||||
/composer.lock
|
||||
|
104
Grocy.php
Normal file
104
Grocy.php
Normal 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;
|
||||
}
|
||||
}
|
@@ -13,8 +13,10 @@ class GrocyDbMigrator
|
||||
qu_id_purchase INTEGER NOT NULL,
|
||||
qu_id_stock INTEGER NOT NULL,
|
||||
qu_factor_purchase_to_stock REAL NOT NULL,
|
||||
barcode TEXT UNIQUE,
|
||||
created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
barcode TEXT,
|
||||
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,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
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,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
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,
|
||||
best_before_date DATE,
|
||||
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, "
|
||||
CREATE TABLE consumptions (
|
||||
CREATE TABLE stock_log (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
product_id INTEGER NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
best_before_date DATE,
|
||||
purchased_date DATE,
|
||||
used_date DATE DEFAULT (datetime('now', 'localtime')),
|
||||
used_date DATE,
|
||||
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,14 +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 ('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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
$pdo->exec(utf8_encode($sql));
|
||||
$pdo->exec('INSERT INTO migrations (migration) VALUES (' . $migrationId . ')');
|
||||
Grocy::ExecuteDbStatement($pdo, $sql);
|
||||
Grocy::ExecuteDbStatement($pdo, 'INSERT INTO migrations (migration) VALUES (' . $migrationId . ')');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,21 +4,56 @@ class GrocyDemoDataGenerator
|
||||
{
|
||||
public static function PopulateDemoData(PDO $pdo)
|
||||
{
|
||||
$pdo->exec(utf8_encode("
|
||||
UPDATE locations SET name = 'Vorratskammer', description = '' WHERE id = 1;
|
||||
INSERT INTO locations (name) VALUES ('S<><53>igkeitenschrank');
|
||||
INSERT INTO locations (name) VALUES ('Konvervenschrank');
|
||||
$rowCount = Grocy::ExecuteDbQuery($pdo, 'SELECT COUNT(*) FROM migrations WHERE migration = -1')->fetchColumn();
|
||||
if (intval($rowCount) === 0)
|
||||
{
|
||||
$sql = "
|
||||
UPDATE locations SET name = 'Vorratskammer', description = '' WHERE id = 1;
|
||||
INSERT INTO locations (name) VALUES ('S<><53>igkeitenschrank'); --2
|
||||
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;
|
||||
INSERT INTO quantity_units (name) VALUES ('Packung');
|
||||
UPDATE quantity_units SET name = 'St<53>ck' WHERE id = 1;
|
||||
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);
|
||||
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) VALUES ('Eier', 1, 2, 1, 10);
|
||||
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, 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, 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 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()."');
|
||||
"));
|
||||
INSERT INTO migrations (migration) VALUES (-1);
|
||||
";
|
||||
|
||||
Grocy::ExecuteDbStatement($pdo, $sql);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,10 +2,20 @@
|
||||
|
||||
class GrocyLogicStock
|
||||
{
|
||||
const TRANSACTION_TYPE_PURCHASE = 'purchase';
|
||||
const TRANSACTION_TYPE_CONSUME = 'consume';
|
||||
const TRANSACTION_TYPE_INVENTORY_CORRECTION = 'inventory-correction';
|
||||
|
||||
public static function GetCurrentStock()
|
||||
{
|
||||
$db = Grocy::GetDbConnectionRaw();
|
||||
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) DESC')->fetchAll(PDO::FETCH_OBJ);
|
||||
$sql = 'SELECT * from stock_current';
|
||||
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)
|
||||
@@ -15,7 +25,7 @@ class GrocyLogicStock
|
||||
$product = $db->products($productId);
|
||||
$productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount');
|
||||
$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);
|
||||
$quStock = $db->quantity_units($product->qu_id_stock);
|
||||
|
||||
@@ -29,61 +39,153 @@ 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();
|
||||
|
||||
$productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount');
|
||||
$potentialStockEntries = $db->stock()->where('product_id', $productId)->orderBy('purchased_date', 'ASC')->fetchAll(); //FIFO
|
||||
|
||||
if ($amount > $productStockAmount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($potentialStockEntries as $stockEntry)
|
||||
{
|
||||
if ($amount == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if ($amount >= $stockEntry->amount) //Take the whole stock entry
|
||||
{
|
||||
$logRow = $db->stock_log()->createRow(array(
|
||||
'product_id' => $stockEntry->product_id,
|
||||
'amount' => $stockEntry->amount * -1,
|
||||
'best_before_date' => $stockEntry->best_before_date,
|
||||
'purchased_date' => $stockEntry->purchased_date,
|
||||
'used_date' => date('Y-m-d'),
|
||||
'spoiled' => $spoiled,
|
||||
'stock_id' => $stockEntry->stock_id,
|
||||
'transaction_type' => $transactionType
|
||||
));
|
||||
$logRow->save();
|
||||
|
||||
$amount -= $stockEntry->amount;
|
||||
$stockEntry->delete();
|
||||
}
|
||||
else //Stock entry amount is > than needed amount -> split the stock entry resp. update the amount
|
||||
{
|
||||
$logRow = $db->stock_log()->createRow(array(
|
||||
'product_id' => $stockEntry->product_id,
|
||||
'amount' => $amount * -1,
|
||||
'best_before_date' => $stockEntry->best_before_date,
|
||||
'purchased_date' => $stockEntry->purchased_date,
|
||||
'used_date' => date('Y-m-d'),
|
||||
'spoiled' => $spoiled,
|
||||
'stock_id' => $stockEntry->stock_id,
|
||||
'transaction_type' => $transactionType
|
||||
));
|
||||
$logRow->save();
|
||||
|
||||
$restStockAmount = $stockEntry->amount - $amount;
|
||||
$amount = 0;
|
||||
|
||||
$stockEntry->update(array(
|
||||
'amount' => $restStockAmount
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
$potentialStockEntries = $db->stock()->where('product_id', $productId)->orderBy('purchased_date', 'ASC')->fetchAll(); //FIFO
|
||||
|
||||
if ($amount > $productStockAmount)
|
||||
if ($newAmount > $productStockAmount)
|
||||
{
|
||||
return false;
|
||||
$amountToAdd = $newAmount - $productStockAmount;
|
||||
self::AddProduct($productId, $amountToAdd, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION);
|
||||
}
|
||||
|
||||
foreach ($potentialStockEntries as $stockEntry)
|
||||
else if ($newAmount < $productStockAmount)
|
||||
{
|
||||
if ($amount == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if ($amount >= $stockEntry->amount) //Take the whole stock entry
|
||||
{
|
||||
$consumptionRow = $db->consumptions()->createRow(array(
|
||||
'product_id' => $stockEntry->product_id,
|
||||
'amount' => $stockEntry->amount,
|
||||
'best_before_date' => $stockEntry->best_before_date,
|
||||
'purchased_date' => $stockEntry->purchased_date,
|
||||
'spoiled' => $spoiled,
|
||||
'stock_id' => $stockEntry->stock_id
|
||||
));
|
||||
$consumptionRow->save();
|
||||
|
||||
$amount -= $stockEntry->amount;
|
||||
$stockEntry->delete();
|
||||
}
|
||||
else //Stock entry amount is > than need amount -> split the stock entry resp. update the amount
|
||||
{
|
||||
$consumptionRow = $db->consumptions()->createRow(array(
|
||||
'product_id' => $stockEntry->product_id,
|
||||
'amount' => $amount,
|
||||
'best_before_date' => $stockEntry->best_before_date,
|
||||
'purchased_date' => $stockEntry->purchased_date,
|
||||
'spoiled' => $spoiled,
|
||||
'stock_id' => $stockEntry->stock_id
|
||||
));
|
||||
$consumptionRow->save();
|
||||
|
||||
$restStockAmount = $stockEntry->amount - $amount;
|
||||
$amount = 0;
|
||||
|
||||
$stockEntry->update(array(
|
||||
'amount' => $restStockAmount
|
||||
));
|
||||
}
|
||||
$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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -14,4 +14,47 @@ class GrocyPhpHelper
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -11,10 +11,7 @@ For now my main focus is on stock management, ERP your fridge!
|
||||
Public demo of the latest version → [https://grocy.projectdemos.berrnd.org](https://grocy.projectdemos.berrnd.org)
|
||||
|
||||
## 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.
|
||||
|
||||
## Todo
|
||||
A lot...
|
||||
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.
|
||||
|
||||
## License
|
||||
The MIT License (MIT)
|
||||
|
@@ -14,6 +14,8 @@
|
||||
"datatables.net-bs": "2.1.1",
|
||||
"datatables.net-responsive": "2.1.1",
|
||||
"datatables.net-responsive-bs": "2.1.1",
|
||||
"jquery-timeago": "1.5.4"
|
||||
"jquery-timeago": "1.5.4",
|
||||
"toastr": "2.1.3",
|
||||
"tagmanager": "3.0.2"
|
||||
}
|
||||
}
|
||||
|
@@ -7,5 +7,5 @@ mkdir "%releasePath%"
|
||||
for /f "tokens=*" %%a in ('type version.txt') do set version=%%a
|
||||
|
||||
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" 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" 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\*.*
|
||||
|
26
grocy.js
26
grocy.js
@@ -75,3 +75,29 @@ Grocy.EmptyElementWhenMatches = function(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));
|
||||
}
|
||||
|
53
grocy.php
53
grocy.php
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
class Grocy
|
||||
{
|
||||
private static $DbConnection;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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');
|
||||
}
|
||||
}
|
@@ -24,6 +24,9 @@
|
||||
<Compile Include="GrocyDbMigrator.php" />
|
||||
<Compile Include="index.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\quantityunitform.php" />
|
||||
<Compile Include="views\locationform.php" />
|
||||
@@ -49,6 +52,9 @@
|
||||
<Content Include="version.txt" />
|
||||
<Content Include="views\consumption.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\quantityunitform.js" />
|
||||
<Content Include="views\locationform.js" />
|
||||
|
185
index.php
185
index.php
@@ -4,13 +4,13 @@ use \Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use \Psr\Http\Message\ResponseInterface as Response;
|
||||
use Slim\Views\PhpRenderer;
|
||||
|
||||
require_once 'vendor/autoload.php';
|
||||
require_once 'config.php';
|
||||
require_once 'Grocy.php';
|
||||
require_once 'GrocyDbMigrator.php';
|
||||
require_once 'GrocyDemoDataGenerator.php';
|
||||
require_once 'GrocyLogicStock.php';
|
||||
require_once 'GrocyPhpHelper.php';
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/data/config.php';
|
||||
require_once __DIR__ . '/Grocy.php';
|
||||
require_once __DIR__ . '/GrocyDbMigrator.php';
|
||||
require_once __DIR__ . '/GrocyDemoDataGenerator.php';
|
||||
require_once __DIR__ . '/GrocyLogicStock.php';
|
||||
require_once __DIR__ . '/GrocyPhpHelper.php';
|
||||
|
||||
$app = new \Slim\App(new \Slim\Container([
|
||||
'settings' => [
|
||||
@@ -32,22 +32,23 @@ if (!Grocy::IsDemoInstallation())
|
||||
]));
|
||||
}
|
||||
|
||||
$app->get('/', function(Request $request, Response $response)
|
||||
$db = Grocy::GetDbConnection();
|
||||
|
||||
$app->get('/', function(Request $request, Response $response) use($db)
|
||||
{
|
||||
$db = Grocy::GetDbConnection();
|
||||
$db = Grocy::GetDbConnection(true); //For database schema migration
|
||||
|
||||
return $this->renderer->render($response, '/layout.php', [
|
||||
'title' => 'Dashboard',
|
||||
'contentPage' => 'dashboard.php',
|
||||
'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', [
|
||||
'title' => 'Purchase',
|
||||
'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', [
|
||||
'title' => 'Consumption',
|
||||
'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', [
|
||||
'title' => 'Products',
|
||||
'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', [
|
||||
'title' => 'Locations',
|
||||
'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', [
|
||||
'title' => 'Quantity units',
|
||||
'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')
|
||||
{
|
||||
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')
|
||||
{
|
||||
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')
|
||||
{
|
||||
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']}());
|
||||
|
||||
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']));
|
||||
|
||||
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->save();
|
||||
$success = $newRow->isClean();
|
||||
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->update($request->getParsedBody());
|
||||
$success = $row->isClean();
|
||||
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->delete();
|
||||
$success = $row->isClean();
|
||||
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']));
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
});
|
||||
$bestBeforeDate = date('Y-m-d');
|
||||
if (isset($request->getQueryParams()['bestbeforedate']) && !empty($request->getQueryParams()['bestbeforedate']))
|
||||
{
|
||||
$bestBeforeDate = $request->getQueryParams()['bestbeforedate'];
|
||||
}
|
||||
|
||||
$this->get('/stock/get-current-stock', function(Request $request, Response $response)
|
||||
{
|
||||
echo json_encode(GrocyLogicStock::GetCurrentStock());
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
$transactionType = GrocyLogicStock::TRANSACTION_TYPE_PURCHASE;
|
||||
if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype']))
|
||||
{
|
||||
$transactionType = $request->getQueryParams()['transactiontype'];
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -245,15 +266,45 @@ $app->group('/api', function()
|
||||
$spoiled = true;
|
||||
}
|
||||
|
||||
echo json_encode(array('success' => GrocyLogicStock::ConsumeProduct($args['productId'], $args['amount'], $spoiled)));
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
$transactionType = GrocyLogicStock::TRANSACTION_TYPE_CONSUME;
|
||||
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()));
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
$bestBeforeDate = date('Y-m-d');
|
||||
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();
|
||||
|
29
style.css
29
style.css
@@ -104,4 +104,31 @@
|
||||
.timeago-contextual {
|
||||
font-style: italic;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled,
|
||||
.no-real-button {
|
||||
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;
|
||||
}
|
||||
|
@@ -1 +1 @@
|
||||
0.2.0
|
||||
1.2.0
|
@@ -10,14 +10,26 @@
|
||||
spoiled = 1;
|
||||
}
|
||||
|
||||
Grocy.FetchJson('/api/stock/consume-product/' + jsonForm.product_id + '/' + jsonForm.amount + '?spoiled=' + spoiled,
|
||||
function(result)
|
||||
Grocy.FetchJson('/api/stock/get-product-details/' + jsonForm.product_id,
|
||||
function (productDetails)
|
||||
{
|
||||
$('#product_id_text_input').focus();
|
||||
$('#product_id_text_input').val('');
|
||||
$('#product_id_text_input').trigger('change');
|
||||
$('#amount').val(1);
|
||||
$('#consumption-form').validator('validate');
|
||||
Grocy.FetchJson('/api/stock/consume-product/' + jsonForm.product_id + '/' + jsonForm.amount + '?spoiled=' + spoiled,
|
||||
function(result)
|
||||
{
|
||||
toastr.success('Removed ' + jsonForm.amount + ' ' + productDetails.quantity_unit_stock.name + ' of ' + productDetails.product.name + ' from stock');
|
||||
|
||||
$('#amount').val(1);
|
||||
$('#product_id').val('');
|
||||
$('#product_id_text_input').focus();
|
||||
$('#product_id_text_input').val('');
|
||||
$('#product_id_text_input').trigger('change');
|
||||
$('#consumption-form').validator('validate');
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
@@ -33,20 +45,41 @@ $('#product_id').on('change', function(e)
|
||||
if (productId)
|
||||
{
|
||||
Grocy.FetchJson('/api/stock/get-product-details/' + productId,
|
||||
function(productStatistics)
|
||||
function (productDetails)
|
||||
{
|
||||
$('#selected-product-name').text(productStatistics.product.name);
|
||||
$('#selected-product-stock-amount').text(productStatistics.stock_amount || '0');
|
||||
$('#selected-product-stock-qu-name').text(productStatistics.quantity_unit_stock.name);
|
||||
$('#selected-product-stock-qu-name2').text(productStatistics.quantity_unit_stock.name);
|
||||
$('#selected-product-last-purchased').text((productStatistics.last_purchased || 'never').substring(0, 10));
|
||||
$('#selected-product-last-purchased-timeago').text($.timeago(productStatistics.last_purchased || ''));
|
||||
$('#selected-product-last-used').text((productStatistics.last_used || 'never').substring(0, 10));
|
||||
$('#selected-product-last-used-timeago').text($.timeago(productStatistics.last_used || ''));
|
||||
$('#amount').attr('max', productStatistics.stock_amount);
|
||||
$('#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').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-used-timeago', 'NaN years ago');
|
||||
|
||||
if ((productDetails.stock_amount || 0) === 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 not in stock.');
|
||||
$('#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)
|
||||
{
|
||||
@@ -58,23 +91,49 @@ $('#product_id').on('change', function(e)
|
||||
|
||||
$(function()
|
||||
{
|
||||
$('.datepicker').datepicker(
|
||||
{
|
||||
format: 'yyyy-mm-dd',
|
||||
startDate: '-3d',
|
||||
todayHighlight: true,
|
||||
autoclose: true,
|
||||
calendarWeeks: true,
|
||||
orientation: 'bottom auto'
|
||||
$('.combobox').combobox({
|
||||
appendId: '_text_input'
|
||||
});
|
||||
$('.datepicker').val(moment().format('YYYY-MM-DD'));
|
||||
$('.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 (possibleOptionElement.length > 0)
|
||||
{
|
||||
$('#product_id').val(possibleOptionElement.val());
|
||||
$('#product_id').data('combobox').refresh();
|
||||
$('#product_id').trigger('change');
|
||||
}
|
||||
});
|
||||
|
||||
$('#amount').val(1);
|
||||
$('#product_id').val('');
|
||||
$('#product_id_text_input').focus();
|
||||
$('#product_id_text_input').val('');
|
||||
$('#product_id_text_input').trigger('change');
|
||||
|
||||
$('#consumption-form').validator();
|
||||
$('#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)
|
||||
{
|
||||
if (event.keyCode === 13) //Enter
|
||||
{
|
||||
if ($('#consumption-form').validator('validate').has('.has-error').length !== 0) //There is at least one validation error
|
||||
{
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@@ -1,38 +1,39 @@
|
||||
<div class="col-sm-3 col-sm-offset-3 col-md-3 col-md-offset-2 main">
|
||||
<h1 class="page-header">Record consumption</h1>
|
||||
|
||||
<h1 class="page-header">Consumption</h1>
|
||||
|
||||
<form id="consumption-form">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="product_id">Product</label>
|
||||
<label for="product_id">Product <i class="fa fa-barcode"></i></label>
|
||||
<select class="form-control combobox" id="product_id" name="product_id" required>
|
||||
<option value=""></option>
|
||||
<?php foreach ($products as $product) : ?>
|
||||
<option value="<?php echo $product->id; ?>"><?php echo $product->name; ?></option>
|
||||
<option data-additional-searchdata="<?php echo $product->barcode; ?>" value="<?php echo $product->id; ?>"><?php echo $product->name; ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="input-group date">
|
||||
<input type="text" class="form-control" id="barcode" name="barcode" />
|
||||
<div class="input-group-addon">
|
||||
<i class="fa fa-barcode"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="help-block with-errors"></div>
|
||||
<div id="product-error" class="help-block with-errors"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="amount">Amount</label>
|
||||
<label for="amount">Amount <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>
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label for="spoiled">
|
||||
<input type="checkbox" id="spoiled" name="spoiled"> Spoiled
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button id="save-consumption-button" type="submit" class="btn btn-default">OK</button>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3 col-md-3 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>
|
||||
<h4><strong>Stock quantity unit:</strong> <span id="selected-product-stock-qu-name"></span></h4>
|
||||
|
||||
@@ -41,4 +42,4 @@
|
||||
<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>
|
||||
</div>
|
||||
|
@@ -1,4 +1,7 @@
|
||||
$(function()
|
||||
{
|
||||
$('#current-stock-table').DataTable({ 'paging': false });
|
||||
$('#current-stock-table').DataTable({
|
||||
'pageLength': 50,
|
||||
'order': [[2, 'asc']]
|
||||
});
|
||||
});
|
||||
|
@@ -1,7 +1,19 @@
|
||||
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
|
||||
|
||||
<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">
|
||||
<table id="current-stock-table" class="table table-striped">
|
||||
<thead>
|
||||
@@ -13,7 +25,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<?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>
|
||||
<?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name; ?>
|
||||
</td>
|
||||
@@ -21,11 +33,13 @@
|
||||
<?php echo $currentStockEntry->amount; ?>
|
||||
</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>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
369
views/inventory.js
Normal file
369
views/inventory.js
Normal 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
52
views/inventory.php
Normal 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 <i class="fa fa-barcode"></i><span id="barcode-lookup-disabled-hint" class="small text-muted hide"> 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 <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 <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>
|
@@ -11,16 +11,18 @@
|
||||
|
||||
<title><?php echo $title; ?> | grocy</title>
|
||||
|
||||
<link href="/bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="/bower_components/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
|
||||
<link href="/bower_components/bootstrap-datepicker/dist/css/bootstrap-datepicker3.min.css" rel="stylesheet" />
|
||||
<link href="/bower_components/bootstrap-combobox/css/bootstrap-combobox.css" rel="stylesheet" />
|
||||
<link href="/bower_components/datatables.net-bs/css/dataTables.bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="/bower_components/datatables.net-responsive-bs/css/responsive.bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="/style.css" rel="stylesheet" />
|
||||
<link href="/bower_components/bootstrap/dist/css/bootstrap.min.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" />
|
||||
<link href="/bower_components/font-awesome/css/font-awesome.min.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" />
|
||||
<link href="/bower_components/bootstrap-datepicker/dist/css/bootstrap-datepicker3.min.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" />
|
||||
<link href="/bower_components/bootstrap-combobox/css/bootstrap-combobox.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/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" />
|
||||
|
||||
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
|
||||
<script src="/grocy.js"></script>
|
||||
<script src="/bower_components/jquery/dist/jquery.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
|
||||
<script src="/grocy.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -36,6 +38,7 @@
|
||||
</div>
|
||||
|
||||
<div id="navbar-mobile" class="navbar-collapse collapse">
|
||||
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li data-nav-for-page="dashboard.php">
|
||||
<a class="discrete-link" href="/"><i class="fa fa-tachometer fa-fw"></i> Dashboard</a>
|
||||
@@ -46,7 +49,14 @@
|
||||
<li data-nav-for-page="consumption.php">
|
||||
<a class="discrete-link" href="/consumption"><i class="fa fa-cutlery fa-fw"></i> Record consumption</a>
|
||||
</li>
|
||||
<li data-nav-for-page="inventory.php">
|
||||
<a class="discrete-link" href="/inventory"><i class="fa fa-list fa-fw"></i> 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> Shopping list</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li data-nav-for-page="products.php">
|
||||
<a class="discrete-link" href="/products"><i class="fa fa-product-hunt fa-fw"></i> Manage products</a>
|
||||
@@ -58,13 +68,16 @@
|
||||
<a class="discrete-link" href="/quantityunits"><i class="fa fa-balance-scale fa-fw"></i> Manage quantity units</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-sm-3 col-md-2 sidebar">
|
||||
|
||||
<ul class="nav nav-sidebar">
|
||||
<li data-nav-for-page="dashboard.php">
|
||||
<a class="discrete-link" href="/"><i class="fa fa-tachometer fa-fw"></i> Dashboard</a>
|
||||
@@ -75,7 +88,14 @@
|
||||
<li data-nav-for-page="consumption.php">
|
||||
<a class="discrete-link" href="/consumption"><i class="fa fa-cutlery fa-fw"></i> Record consumption</a>
|
||||
</li>
|
||||
<li data-nav-for-page="inventory.php">
|
||||
<a class="discrete-link" href="/inventory"><i class="fa fa-list fa-fw"></i> 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> Shopping list</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="nav nav-sidebar">
|
||||
<li data-nav-for-page="products.php">
|
||||
<a class="discrete-link" href="/products"><i class="fa fa-product-hunt fa-fw"></i> Manage products</a>
|
||||
@@ -87,13 +107,14 @@
|
||||
<a class="discrete-link" href="/quantityunits"><i class="fa fa-balance-scale fa-fw"></i> Manage quantity units</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="nav-copyright nav nav-sidebar">
|
||||
grocy is a project by
|
||||
<a class="discrete-link" href="https://berrnd.de" target="_blank">Bernd Bestel</a>
|
||||
<br />
|
||||
Created with passion since 2017
|
||||
<br />
|
||||
Version <?php echo file_get_contents('version.txt'); ?>
|
||||
Version <?php echo Grocy::GetInstalledVersion(); ?>
|
||||
<br />
|
||||
Life runs on code
|
||||
<br />
|
||||
@@ -101,28 +122,32 @@
|
||||
<i class="fa fa-github"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>Grocy.ContentPage = '<?php echo $contentPage; ?>';</script>
|
||||
<?php include $contentPage; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
|
||||
<script src="/bower_components/bootbox/bootbox.js"></script>
|
||||
<script src="/bower_components/jquery.serializeJSON/jquery.serializejson.min.js"></script>
|
||||
<script src="/bower_components/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js"></script>
|
||||
<script src="/bower_components/moment/min/moment.min.js"></script>
|
||||
<script src="/bower_components/bootstrap-validator/dist/validator.min.js"></script>
|
||||
<script src="/bower_components/bootstrap-combobox/js/bootstrap-combobox.js"></script>
|
||||
<script src="/bower_components/datatables.net/js/jquery.dataTables.min.js"></script>
|
||||
<script src="/bower_components/datatables.net-bs/js/dataTables.bootstrap.min.js"></script>
|
||||
<script src="/bower_components/datatables.net-responsive/js/dataTables.responsive.min.js"></script>
|
||||
<script src="/bower_components/datatables.net-responsive-bs/js/responsive.bootstrap.min.js"></script>
|
||||
<script src="/bower_components/jquery-timeago/jquery.timeago.js"></script>
|
||||
<script src="/bower_components/bootstrap/dist/js/bootstrap.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
|
||||
<script src="/bower_components/bootbox/bootbox.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
|
||||
<script src="/bower_components/jquery.serializeJSON/jquery.serializejson.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
|
||||
<script src="/bower_components/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
|
||||
<script src="/bower_components/moment/min/moment.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
|
||||
<script src="/bower_components/bootstrap-validator/dist/validator.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
|
||||
<script src="/bower_components/bootstrap-combobox/js/bootstrap-combobox.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
|
||||
<script src="/bower_components/datatables.net/js/jquery.dataTables.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
|
||||
<script src="/bower_components/datatables.net-bs/js/dataTables.bootstrap.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
|
||||
<script src="/bower_components/datatables.net-responsive/js/dataTables.responsive.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/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))) : ?>
|
||||
<script src="/views/<?php echo str_replace('.php', '.js', $contentPage); ?>"></script>
|
||||
<script src="/views/<?php echo str_replace('.php', '.js', $contentPage) . '?v=' . Grocy::GetInstalledVersion(); ?>"></script>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (file_exists(__DIR__ . '/../data/add_before_end_body.html')) include __DIR__ . '/../data/add_before_end_body.html' ?>
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<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>
|
||||
|
||||
<script>Grocy.EditMode = '<?php echo $mode; ?>';</script>
|
||||
@@ -8,15 +9,20 @@
|
||||
<?php endif; ?>
|
||||
|
||||
<form id="location-form">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<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>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $location->description; ?></textarea>
|
||||
</div>
|
||||
|
||||
<button id="save-location-button" type="submit" class="btn btn-default">Save</button>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
@@ -14,7 +14,7 @@
|
||||
},
|
||||
callback: function(result)
|
||||
{
|
||||
if (result == true)
|
||||
if (result === true)
|
||||
{
|
||||
Grocy.FetchJson('/api/delete-object/locations/' + $(e.target).attr('data-location-id'),
|
||||
function(result)
|
||||
@@ -41,4 +41,3 @@ $(function()
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
|
||||
|
||||
<h1 class="page-header">
|
||||
Locations
|
||||
<a class="btn btn-default" href="/location/new" role="button">
|
||||
@@ -37,4 +38,5 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@@ -2,12 +2,19 @@
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
var redirectDestination = '/products';
|
||||
var returnTo = Grocy.GetUriParam('returnto');
|
||||
if (returnTo !== undefined)
|
||||
{
|
||||
redirectDestination = returnTo + '?createdproduct=' + encodeURIComponent($('#name').val());
|
||||
}
|
||||
|
||||
if (Grocy.EditMode === 'create')
|
||||
{
|
||||
Grocy.PostJson('/api/add-object/products', $('#product-form').serializeJSON(),
|
||||
function(result)
|
||||
{
|
||||
window.location.href = '/products';
|
||||
window.location.href = redirectDestination;
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
@@ -20,7 +27,7 @@
|
||||
Grocy.PostJson('/api/edit-object/products/' + Grocy.EditObjectId, $('#product-form').serializeJSON(),
|
||||
function(result)
|
||||
{
|
||||
window.location.href = '/products';
|
||||
window.location.href = redirectDestination;
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
@@ -32,7 +39,61 @@
|
||||
|
||||
$(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();
|
||||
$('#product-form').validator();
|
||||
$('#product-form').validator('validate');
|
||||
|
||||
var prefillName = Grocy.GetUriParam('prefillname');
|
||||
if (prefillName !== undefined)
|
||||
{
|
||||
$('#name').val(prefillName);
|
||||
$('#name').focus();
|
||||
}
|
||||
|
||||
var prefillBarcode = Grocy.GetUriParam('prefillbarcode');
|
||||
if (prefillBarcode !== undefined)
|
||||
{
|
||||
$('#barcode-taginput').tagsManager('pushTag', prefillBarcode);
|
||||
$('#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();
|
||||
}
|
||||
});
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<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>
|
||||
|
||||
<script>Grocy.EditMode = '<?php echo $mode; ?>';</script>
|
||||
@@ -8,24 +9,24 @@
|
||||
<?php endif; ?>
|
||||
|
||||
<form id="product-form">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<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>
|
||||
<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">
|
||||
<label for="description">Description</label>
|
||||
<textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $product->description; ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group tm-group">
|
||||
<label for="barcode-taginput">Barcode(s) <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">
|
||||
<label for="location_id">Location</label>
|
||||
<select required class="form-control" id="location_id" name="location_id">
|
||||
@@ -35,29 +36,48 @@
|
||||
</select>
|
||||
<div class="help-block with-errors"></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">
|
||||
<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) : ?>
|
||||
<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; ?>
|
||||
</select>
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<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) : ?>
|
||||
<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; ?>
|
||||
</select>
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<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>
|
||||
|
||||
<p id="qu-conversion-info" class="help-block text-muted"></p>
|
||||
|
||||
<button id="save-product-button" type="submit" class="btn btn-default">Save</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
@@ -14,7 +14,7 @@
|
||||
},
|
||||
callback: function(result)
|
||||
{
|
||||
if (result == true)
|
||||
if (result === true)
|
||||
{
|
||||
Grocy.FetchJson('/api/delete-object/products/' + $(e.target).attr('data-product-id'),
|
||||
function(result)
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
|
||||
|
||||
<h1 class="page-header">
|
||||
Products
|
||||
<a class="btn btn-default" href="/product/new" role="button">
|
||||
@@ -13,6 +14,7 @@
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Location</th>
|
||||
<th>Min. stock amount</th>
|
||||
<th>QU purchase</th>
|
||||
<th>QU stock</th>
|
||||
<th>QU factor</th>
|
||||
@@ -36,6 +38,9 @@
|
||||
<td>
|
||||
<?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($locations, 'id', $product->location_id)->name; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo $product->min_stock_amount; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($quantityunits, 'id', $product->qu_id_purchase)->name; ?>
|
||||
</td>
|
||||
@@ -53,4 +58,5 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@@ -3,33 +3,54 @@
|
||||
e.preventDefault();
|
||||
|
||||
var jsonForm = $('#purchase-form').serializeJSON();
|
||||
delete jsonForm.barcode;
|
||||
|
||||
Grocy.FetchJson('/api/get-object/products/' + jsonForm.product_id,
|
||||
function(product)
|
||||
Grocy.FetchJson('/api/stock/get-product-details/' + jsonForm.product_id,
|
||||
function (productDetails)
|
||||
{
|
||||
jsonForm.amount = jsonForm.amount * product.qu_factor_purchase_to_stock;
|
||||
var amount = jsonForm.amount * productDetails.product.qu_factor_purchase_to_stock;
|
||||
|
||||
Grocy.FetchJson('/api/helper/uniqid',
|
||||
function (uniqidResponse)
|
||||
Grocy.FetchJson('/api/stock/add-product/' + jsonForm.product_id + '/' + amount + '?bestbeforedate=' + $('#best_before_date').val(),
|
||||
function(result)
|
||||
{
|
||||
jsonForm.amount = jsonForm.amount * product.qu_factor_purchase_to_stock;
|
||||
jsonForm.stock_id = uniqidResponse.uniqid;
|
||||
|
||||
Grocy.PostJson('/api/add-object/stock', jsonForm,
|
||||
function(result)
|
||||
var addBarcode = Grocy.GetUriParam('addbarcodetoselection');
|
||||
if (addBarcode !== undefined)
|
||||
{
|
||||
var existingBarcodes = productDetails.product.barcode || '';
|
||||
if (existingBarcodes.length === 0)
|
||||
{
|
||||
$('#product_id_text_input').focus();
|
||||
$('#product_id_text_input').val('');
|
||||
$('#product_id_text_input').trigger('change');
|
||||
$('#amount').val(1);
|
||||
$('#purchase-form').validator('validate');
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
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);
|
||||
$('#best_before_date').val('');
|
||||
$('#product_id').val('');
|
||||
$('#product_id_text_input').focus();
|
||||
$('#product_id_text_input').val('');
|
||||
$('#product_id_text_input').trigger('change');
|
||||
$('#purchase-form').validator('validate');
|
||||
}
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
@@ -51,16 +72,22 @@ $('#product_id').on('change', function(e)
|
||||
if (productId)
|
||||
{
|
||||
Grocy.FetchJson('/api/stock/get-product-details/' + productId,
|
||||
function(productStatistics)
|
||||
function(productDetails)
|
||||
{
|
||||
$('#selected-product-name').text(productStatistics.product.name);
|
||||
$('#selected-product-stock-amount').text(productStatistics.stock_amount || '0');
|
||||
$('#selected-product-stock-qu-name').text(productStatistics.quantity_unit_stock.name);
|
||||
$('#selected-product-purchase-qu-name').text(productStatistics.quantity_unit_purchase.name);
|
||||
$('#selected-product-last-purchased').text((productStatistics.last_purchased || 'never').substring(0, 10));
|
||||
$('#selected-product-last-purchased-timeago').text($.timeago(productStatistics.last_purchased || ''));
|
||||
$('#selected-product-last-used').text((productStatistics.last_used || 'never').substring(0, 10));
|
||||
$('#selected-product-last-used-timeago').text($.timeago(productStatistics.last_used || ''));
|
||||
$('#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 || ''));
|
||||
$('#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-used-timeago', 'NaN years ago');
|
||||
@@ -78,20 +105,215 @@ $(function()
|
||||
$('.datepicker').datepicker(
|
||||
{
|
||||
format: 'yyyy-mm-dd',
|
||||
startDate: '+7d',
|
||||
startDate: '+0d',
|
||||
todayHighlight: true,
|
||||
autoclose: true,
|
||||
calendarWeeks: true,
|
||||
orientation: 'bottom auto'
|
||||
orientation: 'bottom auto',
|
||||
weekStart: 1,
|
||||
showOnFocus: false
|
||||
});
|
||||
$('.datepicker').val(moment().format('YYYY-MM-DD'));
|
||||
$('.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);
|
||||
$('#best_before_date').val('');
|
||||
$('#product_id').val('');
|
||||
$('#product_id_text_input').focus();
|
||||
$('#product_id_text_input').val('');
|
||||
$('#product_id_text_input').trigger('change');
|
||||
|
||||
$('#purchase-form').validator();
|
||||
$('#purchase-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.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
$('#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)
|
||||
{
|
||||
if (event.keyCode === 13) //Enter
|
||||
{
|
||||
if ($('#purchase-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');
|
||||
$('#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)
|
||||
{
|
||||
$('.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);
|
||||
$('#purchase-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'));
|
||||
}
|
||||
}
|
||||
|
||||
$('#purchase-form').validator('validate');
|
||||
});
|
||||
|
@@ -1,43 +1,45 @@
|
||||
<div class="col-sm-3 col-sm-offset-3 col-md-3 col-md-offset-2 main">
|
||||
<h1 class="page-header">Record purchase</h1>
|
||||
<div class="col-sm-4 col-sm-offset-3 col-md-3 col-md-offset-2 main">
|
||||
|
||||
<h1 class="page-header">Purchase</h1>
|
||||
|
||||
<form id="purchase-form">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="product_id">Product</label>
|
||||
<label for="product_id">Product <i class="fa fa-barcode"></i><span id="barcode-lookup-disabled-hint" class="small text-muted hide"> 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 value="<?php echo $product->id; ?>"><?php echo $product->name; ?></option>
|
||||
<option data-additional-searchdata="<?php echo $product->barcode; ?>" value="<?php echo $product->id; ?>"><?php echo $product->name; ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="input-group date">
|
||||
<input type="text" class="form-control" id="barcode" name="barcode" />
|
||||
<div class="input-group-addon">
|
||||
<i class="fa fa-barcode"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="amount">Amount</label>
|
||||
<input type="number" class="form-control" id="amount" name="amount" value="1" min="1" required>
|
||||
<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="best_before_date">Best before</label>
|
||||
<div class="input-group date">
|
||||
<input type="text" class="form-control datepicker" id="best_before_date" name="best_before_date" required>
|
||||
<div class="input-group-addon">
|
||||
<input type="text" data-isodate="isodate" class="form-control datepicker" id="best_before_date" name="best_before_date" required 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>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="amount">Amount <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>
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
|
||||
<button id="save-purchase-button" type="submit" class="btn btn-default">OK</button>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3 col-md-3 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>
|
||||
<h4><strong>Purchase quantity:</strong> <span id="selected-product-purchase-qu-name"></span></h4>
|
||||
|
||||
@@ -46,4 +48,4 @@
|
||||
<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>
|
||||
</div>
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<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>
|
||||
|
||||
<script>Grocy.EditMode = '<?php echo $mode; ?>';</script>
|
||||
@@ -8,15 +9,20 @@
|
||||
<?php endif; ?>
|
||||
|
||||
<form id="quantityunit-form">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<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>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $quantityunit->description; ?></textarea>
|
||||
</div>
|
||||
|
||||
<button id="save-quantityunit-button" type="submit" class="btn btn-default">Save</button>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
@@ -14,7 +14,7 @@
|
||||
},
|
||||
callback: function(result)
|
||||
{
|
||||
if (result == true)
|
||||
if (result === true)
|
||||
{
|
||||
Grocy.FetchJson('/api/delete-object/quantity_units/' + $(e.target).attr('data-quantityunit-id'),
|
||||
function(result)
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
|
||||
|
||||
<h1 class="page-header">
|
||||
Quantity units
|
||||
<a class="btn btn-default" href="/quantityunit/new" role="button">
|
||||
@@ -37,4 +38,5 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
38
views/shoppinglist.js
Normal file
38
views/shoppinglist.js
Normal 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
45
views/shoppinglist.php
Normal 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> Add
|
||||
</a>
|
||||
<a id="add-products-below-min-stock-amount" class="btn btn-info" href="#" role="button">
|
||||
<i class="fa fa-plus"></i> 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
147
views/shoppinglistform.js
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
45
views/shoppinglistform.php
Normal file
45
views/shoppinglistform.php
Normal 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 <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 <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>
|
Reference in New Issue
Block a user