Compare commits

...

4 Commits

Author SHA1 Message Date
Bernd Bestel
bd16b8c851 Added flow to directly add articles and barcodes form purchase and inventory view 2017-04-20 22:01:14 +02:00
Bernd Bestel
c4a22c18f7 This is 1.0 2017-04-20 17:10:21 +02:00
Bernd Bestel
e38c24f9ed Going straight to 1.0... 2017-04-19 21:09:28 +02:00
Bernd Bestel
83a7534a74 Hide barcode in select dropdown but search in it 2017-04-18 23:04:26 +02:00
31 changed files with 965 additions and 191 deletions

1
.gitignore vendored
View File

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

View File

@@ -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'))
)"
);

View File

@@ -6,24 +6,50 @@ class GrocyDemoDataGenerator
{
$sql = "
UPDATE locations SET name = 'Vorratskammer', description = '' WHERE id = 1;
INSERT INTO locations (name) VALUES ('S<><53>igkeitenschrank');
INSERT INTO locations (name) VALUES ('Konvervenschrank');
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');
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);
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()."');
DELETE FROM products WHERE id IN (1, 2);
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Gummib<EFBFBD>rchen', 2, 2, 2, 1); --3
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Chips', 2, 2, 2, 1); --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
";
if ($pdo->exec(utf8_encode($sql)) === false)
{
throw new Exception($pdo->errorInfo());
}
GrocyLogicStock::AddProduct(3, 5, date('Y-m-d', strtotime('+180 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(4, 5, date('Y-m-d', strtotime('+180 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(5, 5, date('Y-m-d', strtotime('+20 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(6, 5, date('Y-m-d', strtotime('+600 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(7, 5, date('Y-m-d', strtotime('+800 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(8, 5, date('Y-m-d', strtotime('+900 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(9, 5, date('Y-m-d', strtotime('+14 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(10, 5, date('Y-m-d', strtotime('+21 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(11, 5, date('Y-m-d', strtotime('+10 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(12, 5, date('Y-m-d', strtotime('+2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(13, 5, date('Y-m-d', strtotime('-2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(14, 5, date('Y-m-d', strtotime('+2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(15, 5, date('Y-m-d', strtotime('-2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
}
}

View File

@@ -2,6 +2,10 @@
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();
@@ -15,7 +19,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,59 +33,123 @@ 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 needed 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;

View File

@@ -14,4 +14,36 @@ 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;
}
}

View File

@@ -11,10 +11,7 @@ For now my main focus is on stock management, ERP your fridge!
Public demo of the latest version &rarr; [https://grocy.projectdemos.berrnd.org](https://grocy.projectdemos.berrnd.org)
## 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)

View File

@@ -8,4 +8,4 @@ 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" d "%releasePath%\grocy_%version%.zip" data\add_before_end_body.html data\demo.txt data\config.php data\grocy.db data\.gitignore bower.json

View File

@@ -75,3 +75,23 @@ 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];
}
}
};

View File

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

141
index.php
View File

@@ -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,10 +32,10 @@ if (!Grocy::IsDemoInstallation())
]));
}
$app->get('/', function(Request $request, Response $response)
{
$db = Grocy::GetDbConnection();
$db = Grocy::GetDbConnection();
$app->get('/', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Dashboard',
'contentPage' => 'dashboard.php',
@@ -44,10 +44,8 @@ $app->get('/', function(Request $request, Response $response)
]);
});
$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 +53,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 +62,17 @@ $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('/products', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Products',
'contentPage' => 'products.php',
@@ -79,10 +82,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 +91,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 +100,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 +125,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 +146,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 +167,57 @@ $app->get('/quantityunit/{quantityunitId}', function(Request $request, Response
}
});
$app->group('/api', function()
$app->group('/api', function() use($db, $app)
{
$this->get('/get-objects/{entity}', function(Request $request, Response $response, $args)
$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 +228,39 @@ $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());
});
})->add(function($request, $response, $next)
{
$response = $next($request, $response);
return $response->withHeader('Content-Type', 'application/json');
});
$app->run();

View File

@@ -106,6 +106,15 @@
font-size: 0.8em;
}
.disabled {
.disabled,
.no-real-button {
pointer-events: none;
}
.warning-bg {
background-color: #fcf8e3 !important;
}
.error-bg {
background-color: #f2dede !important;
}

View File

@@ -1 +1 @@
0.4.0
1.0.1

View File

@@ -45,22 +45,23 @@ $('#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');
Grocy.EmptyElementWhenMatches('#selected-product-last-purchased-timeago', 'NaN years ago');
Grocy.EmptyElementWhenMatches('#selected-product-last-used-timeago', 'NaN years ago');
if ((productStatistics.stock_amount || 0) === 0)
if ((productDetails.stock_amount || 0) === 0)
{
$('#product_id').val('');
$('#product_id_text_input').val('');
@@ -69,6 +70,7 @@ $('#product_id').on('change', function(e)
$('#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
{
@@ -88,7 +90,22 @@ $('#product_id').on('change', function(e)
$(function()
{
$('.combobox').combobox({ appendId: '_text_input' });
$('.combobox').combobox({
appendId: '_text_input'
});
$('#product_id_text_input').on('change', function(e)
{
var input = $('#product_id_text_input').val().toString();
var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + input + "']").first();
if (possibleOptionElement.length > 0)
{
$('#product_id').val(possibleOptionElement.val());
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
}
});
$('#amount').val(1);
$('#product_id').val('');
@@ -99,6 +116,14 @@ $(function()
$('#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

View File

@@ -2,27 +2,32 @@
<h1 class="page-header">Consumption</h1>
<form id="consumption-form">
<div class="form-group">
<label for="product_id">Product&nbsp;&nbsp;<i class="fa fa-barcode"></i></label>
<select data-instockproduct="instockproduct" class="form-control combobox" id="product_id" name="product_id" required>
<option value=""></option>
<?php foreach ($products as $product) : ?>
<option value="<?php echo $product->id; ?>"><?php echo $product->name; ?><?php if (!empty($product->barcode)) echo ' [' . $product->barcode . ']'; ?></option>
<option data-additional-searchdata="<?php echo $product->barcode; ?>" value="<?php echo $product->id; ?>"><?php echo $product->name; ?></option>
<?php endforeach; ?>
</select>
<div id="product-error" 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>
<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>
@@ -35,4 +40,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>

View File

@@ -1,7 +1,14 @@
<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</h3>
<div>
<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>
</div>
<div class="table-responsive">
<table id="current-stock-table" class="table table-striped">
<thead>
@@ -13,7 +20,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'; ?>">
<td>
<?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name; ?>
</td>
@@ -21,11 +28,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>

334
views/inventory.js Normal file
View File

@@ -0,0 +1,334 @@
$('#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);
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() { },
buttons: {
cancel: {
label: 'Cancel',
className: 'btn-default',
callback: function() { }
},
addnewproduct: {
label: 'Add as new product',
className: 'btn-success',
callback: function()
{
window.location.href = '/product/new?prefillname=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname);
}
},
addbarcode: {
label: 'Add as barcode to existing product',
className: 'btn-info',
callback: function()
{
window.location.href = '/inventory?addbarcodetoselection=' + encodeURIComponent(input);
}
}
}
});
}
}
});
$('#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';
}
},
'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');
}
});
$('#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 (value.length === 0)
{
element.val(moment().format('YYYY-MM-DD'));
}
else 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);
}
);
}
});

50
views/inventory.php Normal file
View File

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

View File

@@ -38,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>&nbsp;Dashboard</a>
@@ -48,7 +49,11 @@
<li data-nav-for-page="consumption.php">
<a class="discrete-link" href="/consumption"><i class="fa fa-cutlery fa-fw"></i>&nbsp;Record consumption</a>
</li>
<li data-nav-for-page="inventory.php">
<a class="discrete-link" href="/inventory"><i class="fa fa-list fa-fw"></i>&nbsp;Inventory</a>
</li>
</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>&nbsp;Manage products</a>
@@ -60,13 +65,16 @@
<a class="discrete-link" href="/quantityunits"><i class="fa fa-balance-scale fa-fw"></i>&nbsp;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>&nbsp;Dashboard</a>
@@ -77,7 +85,11 @@
<li data-nav-for-page="consumption.php">
<a class="discrete-link" href="/consumption"><i class="fa fa-cutlery fa-fw"></i>&nbsp;Record consumption</a>
</li>
<li data-nav-for-page="inventory.php">
<a class="discrete-link" href="/inventory"><i class="fa fa-list fa-fw"></i>&nbsp;Inventory</a>
</li>
</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>&nbsp;Manage products</a>
@@ -89,6 +101,7 @@
<a class="discrete-link" href="/quantityunits"><i class="fa fa-balance-scale fa-fw"></i>&nbsp;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>
@@ -103,10 +116,12 @@
<i class="fa fa-github"></i>
</a>
</div>
</div>
<script>Grocy.ContentPage = '<?php echo $contentPage; ?>';</script>
<?php include $contentPage; ?>
</div>
</div>

View File

@@ -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>

View File

@@ -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()
]
});
});

View File

@@ -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>

View File

@@ -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)
{
@@ -42,7 +49,7 @@ $(function()
Grocy.FetchJson('/api/get-object/products/' + Grocy.EditObjectId,
function (product)
{
if (product.barcode.length > 0)
if (product.barcode !== null && product.barcode.length > 0)
{
product.barcode.split(',').forEach(function(item)
{
@@ -61,6 +68,13 @@ $(function()
$('#name').focus();
$('#product-form').validator();
$('#product-form').validator('validate');
var prefillName = Grocy.GetUriParam('prefillname');
if (prefillName !== undefined)
{
$('#name').val(prefillName);
$('#name').focus();
}
});
$('.input-group-qu').on('change', function(e)

View File

@@ -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,20 +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="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)</label>
<input type="text" class="form-control tm-input" id="barcode-taginput" placeholder="Add (scan) a barcode here to add one...">
<label for="barcode-taginput">Barcode(s)&nbsp;&nbsp;<i class="fa fa-barcode"></i></label>
<input type="text" class="form-control tm-input" id="barcode-taginput">
<div id="barcode-taginput-container"></div>
</div>
<div class="form-group">
<label for="location_id">Location</label>
<select required class="form-control" id="location_id" name="location_id">
@@ -31,6 +36,19 @@
</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 input-group-qu" id="qu_id_purchase" name="qu_id_purchase">
@@ -40,6 +58,7 @@
</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 input-group-qu" id="qu_id_stock" name="qu_id_stock">
@@ -49,12 +68,16 @@
</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 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>

View File

@@ -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)

View File

@@ -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>

View File

@@ -7,31 +7,49 @@
Grocy.FetchJson('/api/stock/get-product-details/' + jsonForm.product_id,
function (productDetails)
{
jsonForm.amount = jsonForm.amount * productDetails.product.qu_factor_purchase_to_stock;
var amount = jsonForm.amount * productDetails.product.qu_factor_purchase_to_stock;
Grocy.FetchJson('/api/helper/uniqid',
function(uniqidResponse)
Grocy.FetchJson('/api/stock/add-product/' + jsonForm.product_id + '/' + amount + '?bestbeforedate=' + $('#best_before_date').val(),
function(result)
{
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)
{
toastr.success('Added ' + jsonForm.amount + ' ' + productDetails.quantity_unit_stock.name + ' of ' + productDetails.product.name + ' to stock');
$('#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)
{
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');
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)
{
@@ -63,6 +81,12 @@ $('#product_id').on('change', function(e)
$('#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_qu_unit').text(productDetails.quantity_unit_stock.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');
@@ -90,7 +114,57 @@ $(function()
});
$('.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() { },
buttons: {
cancel: {
label: 'Cancel',
className: 'btn-default',
callback: function() { }
},
addnewproduct: {
label: 'Add as new product',
className: 'btn-success',
callback: function()
{
window.location.href = '/product/new?prefillname=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname);
}
},
addbarcode: {
label: 'Add as barcode to existing product',
className: 'btn-info',
callback: function()
{
window.location.href = '/purchase?addbarcodetoselection=' + encodeURIComponent(input);
}
}
}
});
}
}
});
$('#amount').val(1);
$('#best_before_date').val('');
@@ -112,6 +186,14 @@ $(function()
});
$('#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
@@ -123,6 +205,31 @@ $(function()
}
}
});
var prefillProduct = Grocy.GetUriParam('createdproduct');
if (prefillProduct !== undefined)
{
var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + prefillProduct + "']").first();
if (possibleOptionElement.length === 0)
{
possibleOptionElement = $("#product_id option:contains('" + prefillProduct + "')").first();
}
if (possibleOptionElement.length > 0)
{
$('#product_id').val(possibleOptionElement.val());
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
$('#best_before_date').focus();
}
}
var addBarcode = Grocy.GetUriParam('addbarcodetoselection');
if (addBarcode !== undefined)
{
$('#addbarcodetoselection').text(addBarcode);
$('#flow-info-addbarcodetoselection').removeClass('hide');
}
});
$('#best_before_date-datepicker-button').on('click', function(e)

View File

@@ -2,16 +2,19 @@
<h1 class="page-header">Purchase</h1>
<form id="purchase-form">
<div class="form-group">
<label for="product_id">Product&nbsp;&nbsp;<i class="fa fa-barcode"></i></label>
<select class="form-control combobox" id="product_id" name="product_id" required>
<option value=""></option>
<?php foreach ($products as $product) : ?>
<option value="<?php echo $product->id; ?>"><?php echo $product->name; ?><?php if (!empty($product->barcode)) echo ' [' . $product->barcode . ']'; ?></option>
<option data-additional-searchdata="<?php echo $product->barcode; ?>" value="<?php echo $product->id; ?>"><?php echo $product->name; ?></option>
<?php endforeach; ?>
</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="best_before_date">Best before</label>
<div class="input-group date">
@@ -22,12 +25,15 @@
</div>
<div class="help-block with-errors"></div>
</div>
<div class="form-group">
<label for="amount">Amount</label>
<label for="amount">Amount&nbsp;&nbsp;<span id="new_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>
@@ -40,4 +46,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>

View File

@@ -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>

View File

@@ -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)

View File

@@ -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>