mirror of
				https://github.com/grocy/grocy.git
				synced 2025-10-31 02:36:54 +00:00 
			
		
		
		
	Compare commits
	
		
			14 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 8e40c50cc1 | ||
|  | 4b0f0141c9 | ||
|  | d1bd21a601 | ||
|  | c6925ba4c3 | ||
|  | 52e311d847 | ||
|  | f2f18d260d | ||
|  | 1d293741ba | ||
|  | 5db288fc3c | ||
|  | d628f9b3ca | ||
|  | fe8a6d96e4 | ||
|  | bd16b8c851 | ||
|  | c4a22c18f7 | ||
|  | e38c24f9ed | ||
|  | 83a7534a74 | 
							
								
								
									
										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,18 +71,46 @@ class GrocyDbMigrator | ||||
| 			INSERT INTO products (name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('DefaultProduct1', 'This is the first default product, edit or delete it', 1, 1, 1, 1); | ||||
| 			INSERT INTO products (name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('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) | ||||
| 		{ | ||||
| 			if ($pdo->exec(utf8_encode($sql)) === false) | ||||
| 			{ | ||||
| 				throw new Exception($pdo->errorInfo()); | ||||
| 			} | ||||
|  | ||||
| 			$pdo->exec('INSERT INTO migrations (migration) VALUES (' . $migrationId . ')'); | ||||
| 			Grocy::ExecuteDbStatement($pdo, $sql); | ||||
| 			Grocy::ExecuteDbStatement($pdo, 'INSERT INTO migrations (migration) VALUES (' . $migrationId . ')'); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -4,26 +4,56 @@ class GrocyDemoDataGenerator | ||||
| { | ||||
| 	public static function PopulateDemoData(PDO $pdo) | ||||
| 	{ | ||||
| 		$sql = " | ||||
| 			UPDATE locations SET name = 'Vorratskammer', description = '' WHERE id = 1; | ||||
| 			INSERT INTO locations (name) VALUES ('S<><53>igkeitenschrank'); | ||||
| 			INSERT INTO locations (name) VALUES ('Konvervenschrank'); | ||||
|  | ||||
| 			UPDATE quantity_units SET name = 'St<53>ck' WHERE id = 1; | ||||
| 			INSERT INTO quantity_units (name) VALUES ('Packung'); | ||||
|  | ||||
| 			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()."'); | ||||
| 		"; | ||||
|  | ||||
| 		if ($pdo->exec(utf8_encode($sql)) === false) | ||||
| 		$rowCount = Grocy::ExecuteDbQuery($pdo, 'SELECT COUNT(*) FROM migrations WHERE migration = -1')->fetchColumn(); | ||||
| 		if (intval($rowCount) === 0) | ||||
| 		{ | ||||
| 			throw new Exception($pdo->errorInfo()); | ||||
| 			$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'); --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 | ||||
|  | ||||
| 				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 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) ASC')->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 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; | ||||
| 	} | ||||
|  | ||||
| 	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) | ||||
|   | ||||
| @@ -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)); | ||||
| } | ||||
|   | ||||
							
								
								
									
										63
									
								
								grocy.php
									
									
									
									
									
								
							
							
						
						
									
										63
									
								
								grocy.php
									
									
									
									
									
								
							| @@ -1,63 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| class Grocy | ||||
| { | ||||
| 	private static $DbConnectionRaw; | ||||
| 	/** | ||||
| 	 * @return PDO | ||||
| 	 */ | ||||
| 	public static function GetDbConnectionRaw() | ||||
| 	{ | ||||
| 		if (self::$DbConnectionRaw == null) | ||||
| 		{ | ||||
| 			$newDb = !file_exists(__DIR__ . '/data/grocy.db'); | ||||
| 			$pdo = new PDO('sqlite:' . __DIR__ . '/data/grocy.db'); | ||||
| 			$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); | ||||
|  | ||||
| 			if ($newDb) | ||||
| 			{ | ||||
| 				$pdo->exec("CREATE TABLE migrations (migration INTEGER NOT NULL UNIQUE, execution_time_timestamp DATETIME DEFAULT (datetime('now', 'localtime')), PRIMARY KEY(migration)) WITHOUT ROWID"); | ||||
| 				GrocyDbMigrator::MigrateDb($pdo); | ||||
|  | ||||
| 				if (self::IsDemoInstallation()) | ||||
| 				{ | ||||
| 					GrocyDemoDataGenerator::PopulateDemoData($pdo); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			self::$DbConnectionRaw = $pdo; | ||||
| 		} | ||||
|  | ||||
| 		return self::$DbConnectionRaw; | ||||
| 	} | ||||
|  | ||||
| 	private static $DbConnection; | ||||
| 	/** | ||||
| 	 * @return LessQL\Database | ||||
| 	 */ | ||||
| 	public static function GetDbConnection() | ||||
| 	{ | ||||
| 		if (self::$DbConnection == null) | ||||
| 		{ | ||||
| 			self::$DbConnection = new LessQL\Database(self::GetDbConnectionRaw()); | ||||
| 		} | ||||
|  | ||||
| 		return self::$DbConnection; | ||||
| 	} | ||||
|  | ||||
| 	public static function IsDemoInstallation() | ||||
| 	{ | ||||
| 		return file_exists(__DIR__ . '/data/demo.txt'); | ||||
| 	} | ||||
|  | ||||
| 	private static $InstalledVersion; | ||||
| 	public static function GetInstalledVersion() | ||||
| 	{ | ||||
| 		if (self::$InstalledVersion == null) | ||||
| 		{ | ||||
| 			self::$InstalledVersion = file_get_contents(__DIR__ . '/version.txt'); | ||||
| 		} | ||||
|  | ||||
| 		return self::$InstalledVersion; | ||||
| 	} | ||||
| } | ||||
| @@ -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(); | ||||
|   | ||||
							
								
								
									
										25
									
								
								style.css
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								style.css
									
									
									
									
									
								
							| @@ -106,6 +106,29 @@ | ||||
| 	font-size: 0.8em; | ||||
| } | ||||
|  | ||||
| .disabled { | ||||
| .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.4.0 | ||||
| 1.1.0 | ||||
| @@ -45,22 +45,24 @@ $('#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 ((productStatistics.stock_amount || 0) === 0) | ||||
| 				if ((productDetails.stock_amount || 0) === 0) | ||||
| 				{ | ||||
| 					$('#product_id').val(''); | ||||
| 					$('#product_id_text_input').val(''); | ||||
| @@ -69,6 +71,7 @@ $('#product_id').on('change', function(e) | ||||
| 					$('#product_id_text_input').closest('.form-group').addClass('has-error'); | ||||
| 					$('#product-error').text('This product is not in stock.'); | ||||
| 					$('#product-error').show(); | ||||
| 					$('#product_id_text_input').focus(); | ||||
| 				} | ||||
| 				else | ||||
| 				{ | ||||
| @@ -88,7 +91,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 +117,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 | ||||
|   | ||||
| @@ -1,29 +1,36 @@ | ||||
| <div class="col-sm-3 col-sm-offset-3 col-md-3 col-md-offset-2 main"> | ||||
|  | ||||
| 	<h1 class="page-header">Consumption</h1> | ||||
|  | ||||
| 	<form id="consumption-form"> | ||||
|  | ||||
| 		<div class="form-group"> | ||||
| 			<label for="product_id">Product  <i class="fa fa-barcode"></i></label> | ||||
| 			<select data-instockproduct="instockproduct" class="form-control combobox" id="product_id" name="product_id" required> | ||||
| 			<select class="form-control combobox" id="product_id" name="product_id" required> | ||||
| 				<option value=""></option> | ||||
| 				<?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> | ||||
| 			<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-6 col-md-5 col-lg-3 main well"> | ||||
| @@ -35,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,7 +1,7 @@ | ||||
| $(function() | ||||
| { | ||||
| 	$('#current-stock-table').DataTable({ | ||||
| 		'paging': false, | ||||
| 		'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> | ||||
|   | ||||
							
								
								
									
										355
									
								
								views/inventory.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										355
									
								
								views/inventory.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,355 @@ | ||||
| $('#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() { }, | ||||
| 					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); | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 				}).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(); | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	$('#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> | ||||
| @@ -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> Dashboard</a> | ||||
| @@ -48,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> | ||||
| @@ -60,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> | ||||
| @@ -77,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> | ||||
| @@ -89,6 +107,7 @@ | ||||
| 						<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> | ||||
| @@ -103,10 +122,12 @@ | ||||
| 						<i class="fa fa-github"></i> | ||||
| 					</a> | ||||
| 				</div> | ||||
|  | ||||
| 			</div> | ||||
|  | ||||
| 			<script>Grocy.ContentPage = '<?php echo $contentPage; ?>';</script> | ||||
| 			<?php include $contentPage; ?> | ||||
|  | ||||
| 		</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="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) | ||||
| 			{ | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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)  <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> | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -7,31 +7,50 @@ | ||||
| 	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'); | ||||
| 					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) | ||||
| 				{ | ||||
| @@ -63,6 +82,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 || '')); | ||||
| 				$('#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'); | ||||
| @@ -90,7 +115,67 @@ $(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 <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); | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 				}).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(); | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	$('#amount').val(1); | ||||
| 	$('#best_before_date').val(''); | ||||
| @@ -107,11 +192,26 @@ $(function() | ||||
| 				{ | ||||
| 					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 | ||||
| @@ -123,6 +223,32 @@ $(function() | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	var prefillProduct = Grocy.GetUriParam('createdproduct'); | ||||
| 	if (prefillProduct !== undefined) | ||||
| 	{ | ||||
| 		var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + prefillProduct + "']").first(); | ||||
| 		if (possibleOptionElement.length === 0) | ||||
| 		{ | ||||
| 			possibleOptionElement = $("#product_id option:contains('" + prefillProduct + "')").first(); | ||||
| 		} | ||||
|  | ||||
| 		if (possibleOptionElement.length > 0) | ||||
| 		{ | ||||
| 			$('#product_id').val(possibleOptionElement.val()); | ||||
| 			$('#product_id').data('combobox').refresh(); | ||||
| 			$('#product_id').trigger('change'); | ||||
| 			$('#best_before_date').focus(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var addBarcode = Grocy.GetUriParam('addbarcodetoselection'); | ||||
| 	if (addBarcode !== undefined) | ||||
| 	{ | ||||
| 		$('#addbarcodetoselection').text(addBarcode); | ||||
| 		$('#flow-info-addbarcodetoselection').removeClass('hide'); | ||||
| 		$('#barcode-lookup-disabled-hint').removeClass('hide'); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#best_before_date-datepicker-button').on('click', function(e) | ||||
| @@ -149,11 +275,13 @@ $('#best_before_date').on('keypress', function(e) | ||||
|  | ||||
| 	$('.datepicker').datepicker('hide'); | ||||
|  | ||||
| 	if (value.length === 0) | ||||
| 	//If input is empty and any arrow key is pressed, set date to today | ||||
| 	if (value.length === 0 && (e.keyCode === 38 || e.keyCode === 40 || e.keyCode === 37 || e.keyCode === 39)) | ||||
| 	{ | ||||
| 		element.val(moment().format('YYYY-MM-DD')); | ||||
| 		dateObj = moment(new Date(), 'YYYY-MM-DD', true); | ||||
| 	} | ||||
| 	else if (dateObj.isValid()) | ||||
|  | ||||
| 	if (dateObj.isValid()) | ||||
| 	{ | ||||
| 		if (e.keyCode === 38) //Up | ||||
| 		{ | ||||
|   | ||||
| @@ -1,17 +1,21 @@ | ||||
| <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  <i class="fa fa-barcode"></i></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; ?><?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,13 +26,17 @@ | ||||
| 			</div> | ||||
| 			<div 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> | ||||
|  | ||||
| 		<button id="save-purchase-button" type="submit" class="btn btn-default">OK</button> | ||||
|  | ||||
| 	</form> | ||||
|  | ||||
| </div> | ||||
|  | ||||
| <div class="col-sm-6 col-md-5 col-lg-3 main well"> | ||||
| @@ -40,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