DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); } public function GetCurrentStockLocationContent() { $sql = 'SELECT * FROM stock_current_location_content'; return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); } public function GetCurrentStockLocations() { $sql = 'SELECT * FROM stock_current_locations'; return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); } public function GetCurrentProductPrices() { $sql = 'SELECT * FROM products_current_price'; return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); } public function GetMissingProducts() { $sql = 'SELECT * FROM stock_missing_products_including_opened'; if (!GROCY_FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT) { $sql = 'SELECT * FROM stock_missing_products'; } return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); } public function GetProductStockLocations($productId) { return $this->Database->stock_current_locations()->where('product_id', $productId)->fetchAll(); } public function GetProductIdFromBarcode(string $barcode) { $potentialProduct = $this->Database->products()->where("',' || barcode || ',' LIKE '%,' || :1 || ',%' AND IFNULL(barcode, '') != ''", $barcode)->limit(1)->fetch(); if ($potentialProduct === null) { throw new \Exception("No product with barcode $barcode found"); } return intval($potentialProduct->id); } public function GetExpiringProducts(int $days = 5, bool $excludeExpired = false) { $currentStock = $this->GetCurrentStock(false); $currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d 23:59:59', strtotime("+$days days")), '<'); if ($excludeExpired) { $currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d 23:59:59', strtotime('now')), '>'); } return $currentStock; } public function GetProductDetails(int $productId) { if (!$this->ProductExists($productId)) { throw new \Exception('Product does not exist'); } $stockCurrentRow = FindObjectinArrayByPropertyValue($this->GetCurrentStock(), 'product_id', $productId); if ($stockCurrentRow == null) { $stockCurrentRow = new \stdClass(); $stockCurrentRow->amount = 0; $stockCurrentRow->amount_opened = 0; $stockCurrentRow->amount_aggregated = 0; $stockCurrentRow->amount_opened_aggregated = 0; $stockCurrentRow->is_aggregated_amount = 0; } $product = $this->Database->products($productId); $productLastPurchased = $this->Database->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_PURCHASE)->where('undone', 0)->max('purchased_date'); $productLastUsed = $this->Database->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->where('undone', 0)->max('used_date'); $nextBestBeforeDate = $this->Database->stock()->where('product_id', $productId)->min('best_before_date'); $quPurchase = $this->Database->quantity_units($product->qu_id_purchase); $quStock = $this->Database->quantity_units($product->qu_id_stock); $location = $this->Database->locations($product->location_id); $averageShelfLifeDays = intval($this->Database->stock_average_product_shelf_life()->where('id', $productId)->fetch()->average_shelf_life_days); $lastPrice = null; $lastLogRow = $this->Database->stock_log()->where('product_id = :1 AND transaction_type IN (:2, :3) AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE, self::TRANSACTION_TYPE_INVENTORY_CORRECTION)->orderBy('row_created_timestamp', 'DESC')->limit(1)->fetch(); if ($lastLogRow !== null && !empty($lastLogRow)) { $lastPrice = $lastLogRow->price; } $consumeCount = $this->Database->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->where('undone = 0 AND spoiled = 0')->sum('amount') * -1; $consumeCountSpoiled = $this->Database->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->where('undone = 0 AND spoiled = 1')->sum('amount') * -1; if ($consumeCount == 0) { $consumeCount = 1; } $spoilRate = ($consumeCountSpoiled * 100) / $consumeCount; return array( 'product' => $product, 'last_purchased' => $productLastPurchased, 'last_used' => $productLastUsed, 'stock_amount' => $stockCurrentRow->amount, 'stock_amount_opened' => $stockCurrentRow->amount_opened, 'stock_amount_aggregated' => $stockCurrentRow->amount_aggregated, 'stock_amount_opened_aggregated' => $stockCurrentRow->amount_opened_aggregated, 'quantity_unit_purchase' => $quPurchase, 'quantity_unit_stock' => $quStock, 'last_price' => $lastPrice, 'next_best_before_date' => $nextBestBeforeDate, 'location' => $location, 'average_shelf_life_days' => $averageShelfLifeDays, 'spoil_rate_percent' => $spoilRate, 'is_aggregated_amount' => $stockCurrentRow->is_aggregated_amount ); } public function GetProductPriceHistory(int $productId) { if (!$this->ProductExists($productId)) { throw new \Exception('Product does not exist'); } $returnData = array(); $rows = $this->Database->stock_log()->where('product_id = :1 AND transaction_type IN (:2, :3) AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE, self::TRANSACTION_TYPE_INVENTORY_CORRECTION)->whereNOT('price', null)->orderBy('purchased_date', 'DESC'); foreach ($rows as $row) { $returnData[] = array( 'date' => $row->purchased_date, 'price' => $row->price ); } return $returnData; } public function GetStockEntry($entryId) { return $this->Database->stock()->where('id', $entryId)->fetch(); } public function GetProductStockEntries($productId, $excludeOpened = false) { // In order of next use: // First expiring first, then first in first out if ($excludeOpened) { return $this->Database->stock()->where('product_id = :1 AND open = 0', $productId)->orderBy('best_before_date', 'ASC')->orderBy('purchased_date', 'ASC')->fetchAll(); } else { return $this->Database->stock()->where('product_id', $productId)->orderBy('best_before_date', 'ASC')->orderBy('purchased_date', 'ASC')->fetchAll(); } } public function GetProductStockEntriesForLocation($productId, $locationId, $excludeOpened = false) { $stockEntries = $this->GetProductStockEntries($productId, $excludeOpened); return FindAllObjectsInArrayByPropertyValue($stockEntries, 'location_id', $locationId); } public function AddProduct(int $productId, float $amount, $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId = null, &$transactionId = null) { if (!$this->ProductExists($productId)) { throw new \Exception('Product does not exist'); } // Tare weight handling // The given amount is the new total amount including the container weight (gross) // The amount to be posted needs to be the given amount - stock amount - tare weight $productDetails = (object)$this->GetProductDetails($productId); if ($productDetails->product->enable_tare_weight_handling == 1) { if ($amount <= floatval($productDetails->product->tare_weight) + floatval($productDetails->stock_amount)) { throw new \Exception('The amount cannot be lower or equal than the defined tare weight + current stock amount'); } $amount = $amount - floatval($productDetails->stock_amount) - floatval($productDetails->product->tare_weight); } //Sets the default best before date, if none is supplied if ($bestBeforeDate == null) { if (intval($productDetails->product->default_best_before_days) == -1) { $bestBeforeDate = date('2999-12-31'); } else if (intval($productDetails->product->default_best_before_days) > 0) { $bestBeforeDate = date('Y-m-d', strtotime(date('Y-m-d') . ' + '.$productDetails->product->default_best_before_days.' days')); } else { $bestBeforeDate = date('Y-m-d'); } } if ($transactionType === self::TRANSACTION_TYPE_PURCHASE || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION) { if ($transactionId === null) { $transactionId = uniqid(); } $stockId = uniqid(); $logRow = $this->Database->stock_log()->createRow(array( 'product_id' => $productId, 'amount' => $amount, 'best_before_date' => $bestBeforeDate, 'purchased_date' => $purchasedDate, 'stock_id' => $stockId, 'transaction_type' => $transactionType, 'price' => $price, 'location_id' => $locationId, 'transaction_id' => $transactionId )); $logRow->save(); $returnValue = $this->Database->lastInsertId(); $stockRow = $this->Database->stock()->createRow(array( 'product_id' => $productId, 'amount' => $amount, 'best_before_date' => $bestBeforeDate, 'purchased_date' => $purchasedDate, 'stock_id' => $stockId, 'price' => $price, 'location_id' => $locationId )); $stockRow->save(); return $returnValue; } else { throw new \Exception("Transaction type $transactionType is not valid (StockService.AddProduct)"); } } public function ConsumeProduct(int $productId, float $amount, bool $spoiled, $transactionType, $specificStockEntryId = 'default', $recipeId = null, $locationId = null, &$transactionId = null) { if (!$this->ProductExists($productId)) { throw new \Exception('Product does not exist'); } if ($locationId !== null & !$this->LocationExists($locationId)) { throw new \Exception('Location does not exist'); } // Tare weight handling // The given amount is the new total amount including the container weight (gross) // The amount to be posted needs to be the absolute value of the given amount - stock amount - tare weight $productDetails = (object)$this->GetProductDetails($productId); if ($productDetails->product->enable_tare_weight_handling == 1) { if ($amount < floatval($productDetails->product->tare_weight)) { throw new \Exception('The amount cannot be lower than the defined tare weight'); } $amount = abs($amount - floatval($productDetails->stock_amount) - floatval($productDetails->product->tare_weight)); } if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION) { if ($locationId === null) // Consume from any location { $productStockAmount = $this->Database->stock()->where('product_id', $productId)->sum('amount'); $potentialStockEntries = $this->GetProductStockEntries($productId); } else // Consume only from the supplied location { $productStockAmount = $this->Database->stock()->where('product_id = :1 AND location_id = :2', $productId, $locationId)->sum('amount'); $potentialStockEntries = $this->GetProductStockEntriesForLocation($productId, $locationId); } if ($amount > $productStockAmount) { throw new \Exception('Amount to be consumed cannot be > current stock amount (if supplied, at the desired location)'); } if ($specificStockEntryId !== 'default') { $potentialStockEntries = FindAllObjectsInArrayByPropertyValue($potentialStockEntries, 'stock_id', $specificStockEntryId); } if ($transactionId === null) { $transactionId = uniqid(); } foreach ($potentialStockEntries as $stockEntry) { if ($amount == 0) { break; } if ($amount >= $stockEntry->amount) // Take the whole stock entry { $logRow = $this->Database->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, 'price' => $stockEntry->price, 'opened_date' => $stockEntry->opened_date, 'recipe_id' => $recipeId, 'transaction_id' => $transactionId )); $logRow->save(); $stockEntry->delete(); $amount -= $stockEntry->amount; } else // Stock entry amount is > than needed amount -> split the stock entry resp. update the amount { $restStockAmount = $stockEntry->amount - $amount; $logRow = $this->Database->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, 'price' => $stockEntry->price, 'opened_date' => $stockEntry->opened_date, 'recipe_id' => $recipeId, 'transaction_id' => $transactionId )); $logRow->save(); $stockEntry->update(array( 'amount' => $restStockAmount )); $amount = 0; } } return $this->Database->lastInsertId(); } else { throw new Exception("Transaction type $transactionType is not valid (StockService.ConsumeProduct)"); } } public function TransferProduct(int $productId, float $amount, int $locationIdFrom, int $locationIdTo, $specificStockEntryId = 'default', &$transactionId = null) { if (!$this->ProductExists($productId)) { throw new \Exception('Product does not exist'); } if (!$this->LocationExists($locationIdFrom)) { throw new \Exception('Source location does not exist'); } if (!$this->LocationExists($locationIdTo)) { throw new \Exception('Destination location does not exist'); } // Tare weight handling // The given amount is the new total amount including the container weight (gross) // The amount to be posted needs to be the absolute value of the given amount - stock amount - tare weight $productDetails = (object)$this->GetProductDetails($productId); if ($productDetails->product->enable_tare_weight_handling == 1) { // Hard fail for now, as we not yet support transfering tare weight enabled products throw new \Exception('Transfering tare weight enabled products is not yet possible'); if ($amount < floatval($productDetails->product->tare_weight)) { throw new \Exception('The amount cannot be lower than the defined tare weight'); } $amount = abs($amount - floatval($productDetails->stock_amount) - floatval($productDetails->product->tare_weight)); } $productStockAmountAtFromLocation = $this->Database->stock()->where('product_id = :1 AND location_id = :2', $productId, $locationIdFrom)->sum('amount'); $potentialStockEntriesAtFromLocation = $this->GetProductStockEntriesForLocation($productId, $locationIdFrom); if ($amount > $productStockAmountAtFromLocation) { throw new \Exception('Amount to be transfered cannot be > current stock amount at the source location'); } if ($specificStockEntryId !== 'default') { $potentialStockEntriesAtFromLocation = FindAllObjectsInArrayByPropertyValue($potentialStockEntriesAtFromLocation, 'stock_id', $specificStockEntryId); } if ($transactionId === null) { $transactionId = uniqid(); } foreach ($potentialStockEntriesAtFromLocation as $stockEntry) { if ($amount == 0) { break; } $correlationId = uniqid(); if ($amount >= $stockEntry->amount) // Take the whole stock entry { $logRowForLocationFrom = $this->Database->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, 'stock_id' => $stockEntry->stock_id, 'transaction_type' => self::TRANSACTION_TYPE_TRANSFER_FROM, 'price' => $stockEntry->price, 'opened_date' => $stockEntry->opened_date, 'location_id' => $stockEntry->location_id, 'correlation_id' => $correlationId, 'transaction_Id' => $transactionId )); $logRowForLocationFrom->save(); $logRowForLocationTo = $this->Database->stock_log()->createRow(array( 'product_id' => $stockEntry->product_id, 'amount' => $stockEntry->amount, 'best_before_date' => $stockEntry->best_before_date, 'purchased_date' => $stockEntry->purchased_date, 'stock_id' => $stockEntry->stock_id, 'transaction_type' => self::TRANSACTION_TYPE_TRANSFER_TO, 'price' => $stockEntry->price, 'opened_date' => $stockEntry->opened_date, 'location_id' => $locationIdTo, 'correlation_id' => $correlationId, 'transaction_Id' => $transactionId )); $logRowForLocationTo->save(); $stockEntry->update(array( 'location_id' => $locationIdTo )); $amount -= $stockEntry->amount; } else // Stock entry amount is > than needed amount -> split the stock entry resp. update the amount { $restStockAmount = $stockEntry->amount - $amount; $logRowForLocationFrom = $this->Database->stock_log()->createRow(array( 'product_id' => $stockEntry->product_id, 'amount' => $amount * -1, 'best_before_date' => $stockEntry->best_before_date, 'purchased_date' => $stockEntry->purchased_date, 'stock_id' => $stockEntry->stock_id, 'transaction_type' => self::TRANSACTION_TYPE_TRANSFER_FROM, 'price' => $stockEntry->price, 'opened_date' => $stockEntry->opened_date, 'location_id' => $stockEntry->location_id, 'correlation_id' => $correlationId, 'transaction_Id' => $transactionId )); $logRowForLocationFrom->save(); $logRowForLocationTo = $this->Database->stock_log()->createRow(array( 'product_id' => $stockEntry->product_id, 'amount' => $amount, 'best_before_date' => $stockEntry->best_before_date, 'purchased_date' => $stockEntry->purchased_date, 'stock_id' => $stockEntry->stock_id, 'transaction_type' => self::TRANSACTION_TYPE_TRANSFER_TO, 'price' => $stockEntry->price, 'opened_date' => $stockEntry->opened_date, 'location_id' => $locationIdTo, 'correlation_id' => $correlationId, 'transaction_Id' => $transactionId )); $logRowForLocationTo->save(); // This is the existing stock entry -> remains at the source location with the rest amount $stockEntry->update(array( 'amount' => $restStockAmount )); // The transfered amount gets into a new stock entry $stockEntryNew = $this->Database->stock()->createRow(array( 'product_id' => $stockEntry->product_id, 'amount' => $amount, 'best_before_date' => $stockEntry->best_before_date, 'purchased_date' => $stockEntry->purchased_date, 'stock_id' => $stockEntry->stock_id, 'price' => $stockEntry->price, 'location_id' => $locationIdTo, 'open' => $stockEntry->open, 'opened_date' => $stockEntry->opened_date )); $stockEntryNew->save(); $amount = 0; } } return $this->Database->lastInsertId(); } public function EditStock(int $stockRowId, int $amount, $bestBeforeDate, $locationId, $price) { $stockRow = $this->Database->stock()->where('id = :1', $stockRowId)->fetch(); if ($stockRow === null) { throw new \Exception('Stock does not exist'); } $correlationId = uniqid(); $transactionId = uniqid(); $logOldRowForStockUpdate = $this->Database->stock_log()->createRow(array( 'product_id' => $stockRow->product_id, 'amount' => $stockRow->amount, 'best_before_date' => $stockRow->best_before_date, 'purchased_date' => $stockRow->purchased_date, 'stock_id' => $stockRow->stock_id, 'transaction_type' => self::TRANSACTION_TYPE_STOCK_EDIT_OLD, 'price' => $stockRow->price, 'opened_date' => $stockRow->opened_date, 'location_id' => $stockRow->location_id, 'correlation_id' => $correlationId, 'transaction_id' => $transactionId, 'stock_row_id' => $stockRow->id )); $logOldRowForStockUpdate->save(); $stockRow->update(array( 'amount' => $amount, 'price' => $price, 'best_before_date' => $bestBeforeDate, 'location_id' => $locationId )); $logNewRowForStockUpdate = $this->Database->stock_log()->createRow(array( 'product_id' => $stockRow->product_id, 'amount' => $amount, 'best_before_date' => $bestBeforeDate, 'purchased_date' => $stockRow->purchased_date, 'stock_id' => $stockRow->stock_id, 'transaction_type' => self::TRANSACTION_TYPE_STOCK_EDIT_NEW, 'price' => $price, 'opened_date' => $stockRow->opened_date, 'location_id' => $locationId, 'correlation_id' => $correlationId, 'transaction_id' => $transactionId, 'stock_row_id' => $stockRow->id )); $logNewRowForStockUpdate->save(); $returnValue = $this->Database->lastInsertId(); return $returnValue; } public function InventoryProduct(int $productId, float $newAmount, $bestBeforeDate, $locationId = null, $price = null) { if (!$this->ProductExists($productId)) { throw new \Exception('Product does not exist'); } $productDetails = (object)$this->GetProductDetails($productId); if ($price === null) { $price = $productDetails->last_price; } // Tare weight handling // The given amount is the new total amount including the container weight (gross) // So assume that the amount in stock is the amount also including the container weight $containerWeight = 0; if ($productDetails->product->enable_tare_weight_handling == 1) { $containerWeight = floatval($productDetails->product->tare_weight); } if ($newAmount == floatval($productDetails->stock_amount) + $containerWeight) { throw new \Exception('The new amount cannot equal the current stock amount'); } else if ($newAmount > floatval($productDetails->stock_amount) + $containerWeight) { $bookingAmount = $newAmount - floatval($productDetails->stock_amount); if ($productDetails->product->enable_tare_weight_handling == 1) { $bookingAmount = $newAmount; } return $this->AddProduct($productId, $bookingAmount, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION, date('Y-m-d'), $price, $locationId); } else if ($newAmount < $productDetails->stock_amount + $containerWeight) { $bookingAmount = $productDetails->stock_amount - $newAmount; if ($productDetails->product->enable_tare_weight_handling == 1) { $bookingAmount = $newAmount; } return $this->ConsumeProduct($productId, $bookingAmount, false, self::TRANSACTION_TYPE_INVENTORY_CORRECTION); } return null; } public function OpenProduct(int $productId, float $amount, $specificStockEntryId = 'default') { if (!$this->ProductExists($productId)) { throw new \Exception('Product does not exist'); } $productStockAmountUnopened = $this->Database->stock()->where('product_id = :1 AND open = 0', $productId)->sum('amount'); $potentialStockEntries = $this->GetProductStockEntries($productId, true); $product = $this->Database->products($productId); if ($amount > $productStockAmountUnopened) { throw new \Exception('Amount to be opened cannot be > current unopened stock amount'); } if ($specificStockEntryId !== 'default') { $potentialStockEntries = FindAllObjectsInArrayByPropertyValue($potentialStockEntries, 'stock_id', $specificStockEntryId); } foreach ($potentialStockEntries as $stockEntry) { if ($amount == 0) { break; } $newBestBeforeDate = $stockEntry->best_before_date; if ($product->default_best_before_days_after_open > 0) { $newBestBeforeDate = date("Y-m-d", strtotime('+' . $product->default_best_before_days_after_open . ' days')); } if ($amount >= $stockEntry->amount) // Mark the whole stock entry as opened { $logRow = $this->Database->stock_log()->createRow(array( 'product_id' => $stockEntry->product_id, 'amount' => $stockEntry->amount, 'best_before_date' => $stockEntry->best_before_date, 'purchased_date' => $stockEntry->purchased_date, 'stock_id' => $stockEntry->stock_id, 'transaction_type' => self::TRANSACTION_TYPE_PRODUCT_OPENED, 'price' => $stockEntry->price, 'opened_date' => date('Y-m-d') )); $logRow->save(); $stockEntry->update(array( 'open' => 1, 'opened_date' => date('Y-m-d'), 'best_before_date' => $newBestBeforeDate )); $amount -= $stockEntry->amount; } else // Stock entry amount is > than needed amount -> split the stock entry { $restStockAmount = $stockEntry->amount - $amount; $newStockRow = $this->Database->stock()->createRow(array( 'product_id' => $stockEntry->product_id, 'amount' => $restStockAmount, 'best_before_date' => $stockEntry->best_before_date, 'purchased_date' => $stockEntry->purchased_date, 'stock_id' => $stockEntry->stock_id, 'price' => $stockEntry->price )); $newStockRow->save(); $logRow = $this->Database->stock_log()->createRow(array( 'product_id' => $stockEntry->product_id, 'amount' => $amount, 'best_before_date' => $stockEntry->best_before_date, 'purchased_date' => $stockEntry->purchased_date, 'stock_id' => $stockEntry->stock_id, 'transaction_type' => self::TRANSACTION_TYPE_PRODUCT_OPENED, 'price' => $stockEntry->price, 'opened_date' => date('Y-m-d') )); $logRow->save(); $stockEntry->update(array( 'amount' => $amount, 'open' => 1, 'opened_date' => date('Y-m-d'), 'best_before_date' => $newBestBeforeDate )); $amount = 0; } } return $this->Database->lastInsertId(); } public function AddMissingProductsToShoppingList($listId = 1) { if (!$this->ShoppingListExists($listId)) { throw new \Exception('Shopping list does not exist'); } $missingProducts = $this->GetMissingProducts(); foreach ($missingProducts as $missingProduct) { $product = $this->Database->products()->where('id', $missingProduct->id)->fetch(); $amountToAdd = ceil($missingProduct->amount_missing / $product->qu_factor_purchase_to_stock); $alreadyExistingEntry = $this->Database->shopping_list()->where('product_id', $missingProduct->id)->fetch(); if ($alreadyExistingEntry) // Update { if ($alreadyExistingEntry->amount < $amountToAdd) { $alreadyExistingEntry->update(array( 'amount' => $amountToAdd, 'shopping_list_id' => $listId )); } } else // Insert { $shoppinglistRow = $this->Database->shopping_list()->createRow(array( 'product_id' => $missingProduct->id, 'amount' => $amountToAdd, 'shopping_list_id' => $listId )); $shoppinglistRow->save(); } } } public function ClearShoppingList($listId = 1) { if (!$this->ShoppingListExists($listId)) { throw new \Exception('Shopping list does not exist'); } $this->Database->shopping_list()->where('shopping_list_id = :1', $listId)->delete(); } public function RemoveProductFromShoppingList($productId, $amount = 1, $listId = 1) { if (!$this->ShoppingListExists($listId)) { throw new \Exception('Shopping list does not exist'); } $productRow = $this->Database->shopping_list()->where('product_id = :1', $productId)->fetch(); //If no entry was found with for this product, we return gracefully if ($productRow != null && !empty($productRow)) { $newAmount = $productRow->amount - $amount; if ($newAmount < 1) { $productRow->delete(); } else { $productRow->update(array('amount' => $newAmount)); } } } public function AddProductToShoppingList($productId, $amount = 1, $note = null, $listId = 1) { if (!$this->ShoppingListExists($listId)) { throw new \Exception('Shopping list does not exist'); } if (!$this->ProductExists($productId)) { throw new \Exception('Product does not exist'); } $alreadyExistingEntry = $this->Database->shopping_list()->where('product_id = :1 AND shopping_list_id = :2', $productId, $listId)->fetch(); if ($alreadyExistingEntry) // Update { $alreadyExistingEntry->update(array( 'amount' => ($alreadyExistingEntry->amount + $amount), 'shopping_list_id' => $listId, 'note' => $note )); } else // Insert { $shoppinglistRow = $this->Database->shopping_list()->createRow(array( 'product_id' => $productId, 'amount' => $amount, 'shopping_list_id' => $listId, 'note' => $note )); $shoppinglistRow->save(); } } private function ProductExists($productId) { $productRow = $this->Database->products()->where('id = :1', $productId)->fetch(); return $productRow !== null; } private function LocationExists($locationId) { $locationRow = $this->Database->locations()->where('id = :1', $locationId)->fetch(); return $locationRow !== null; } private function ShoppingListExists($listId) { $shoppingListRow = $this->Database->shopping_lists()->where('id = :1', $listId)->fetch(); return $shoppingListRow !== null; } private function LoadBarcodeLookupPlugin() { $pluginName = defined('GROCY_STOCK_BARCODE_LOOKUP_PLUGIN') ? GROCY_STOCK_BARCODE_LOOKUP_PLUGIN : ''; if (empty($pluginName)) { throw new \Exception('No barcode lookup plugin defined'); } $path = GROCY_DATAPATH . "/plugins/$pluginName.php"; if (file_exists($path)) { require_once $path; return new $pluginName($this->Database->locations()->fetchAll(), $this->Database->quantity_units()->fetchAll()); } else { throw new \Exception("Plugin $pluginName was not found"); } } public function ExternalBarcodeLookup($barcode, $addFoundProduct) { $plugin = $this->LoadBarcodeLookupPlugin(); $pluginOutput = $plugin->Lookup($barcode); if ($pluginOutput !== null) // Lookup was successful { if ($addFoundProduct === true) { // Add product to database and include new product id in output $newRow = $this->Database->products()->createRow($pluginOutput); $newRow->save(); $pluginOutput['id'] = $newRow->id; } } return $pluginOutput; } public function UndoBooking($bookingId, $skipCorrelatedBookings = false) { $logRow = $this->Database->stock_log()->where('id = :1 AND undone = 0', $bookingId)->fetch(); if ($logRow == null) { throw new \Exception('Booking does not exist or was already undone'); } // Undo all correlated bookings first, in order from newest first to the oldest if (!$skipCorrelatedBookings && !empty($logRow->correlation_id)) { $correlatedBookings = $this->Database->stock_log()->where('undone = 0 AND correlation_id = :1', $logRow->correlation_id)->orderBy('id', 'DESC')->fetchAll(); foreach ($correlatedBookings as $correlatedBooking) { $this->UndoBooking($correlatedBooking->id, true); } return; } $hasSubsequentBookings = $this->Database->stock_log()->where('stock_id = :1 AND id != :2 AND (correlation_id is not null OR correlation_id != :3) AND id > :2 AND undone = 0', $logRow->stock_id, $logRow->id, $logRow->correlation_id)->count() > 0; if ($hasSubsequentBookings) { throw new \Exception('Booking has subsequent dependent bookings, undo not possible'); } if ($logRow->transaction_type === self::TRANSACTION_TYPE_PURCHASE || ($logRow->transaction_type === self::TRANSACTION_TYPE_INVENTORY_CORRECTION && $logRow->amount > 0)) { // Remove corresponding stock entry $stockRows = $this->Database->stock()->where('stock_id', $logRow->stock_id); $stockRows->delete(); // Update log entry $logRow->update(array( 'undone' => 1, 'undone_timestamp' => date('Y-m-d H:i:s') )); } elseif ($logRow->transaction_type === self::TRANSACTION_TYPE_CONSUME || ($logRow->transaction_type === self::TRANSACTION_TYPE_INVENTORY_CORRECTION && $logRow->amount < 0)) { // Add corresponding amount back to stock $stockRow = $this->Database->stock()->createRow(array( 'product_id' => $logRow->product_id, 'amount' => $logRow->amount * -1, 'best_before_date' => $logRow->best_before_date, 'purchased_date' => $logRow->purchased_date, 'stock_id' => $logRow->stock_id, 'price' => $logRow->price, 'opened_date' => $logRow->opened_date )); $stockRow->save(); // Update log entry $logRow->update(array( 'undone' => 1, 'undone_timestamp' => date('Y-m-d H:i:s') )); } elseif ($logRow->transaction_type === self::TRANSACTION_TYPE_TRANSFER_TO) { $stockRow = $this->Database->stock()->where('stock_id = :1 AND location_id = :2', $logRow->stock_id, $logRow->location_id)->fetch(); if ($stockRow === null) { throw new \Exception('Booking does not exist or was already undone'); } $newAmount = $stockRow->amount - $logRow->amount; if ($newAmount == 0) { $stockRow->delete(); } else { // Remove corresponding amount back to stock $stockRow->update(array( 'amount' => $newAmount )); } // Update log entry $logRow->update(array( 'undone' => 1, 'undone_timestamp' => date('Y-m-d H:i:s') )); } elseif ($logRow->transaction_type === self::TRANSACTION_TYPE_TRANSFER_FROM) { // Add corresponding amount back to stock or // create a row if missing $stockRow = $this->Database->stock()->where('stock_id = :1 AND location_id = :2', $logRow->stock_id, $logRow->location_id)->fetch(); if ($stockRow === null) { $stockRow = $this->Database->stock()->createRow(array( 'product_id' => $logRow->product_id, 'amount' => $logRow->amount * -1, 'best_before_date' => $logRow->best_before_date, 'purchased_date' => $logRow->purchased_date, 'stock_id' => $logRow->stock_id, 'price' => $logRow->price, 'opened_date' => $logRow->opened_date )); $stockRow->save(); } else { $stockRow->update(array( 'amount' => $stockRow->amount - $logRow->amount )); } // Update log entry $logRow->update(array( 'undone' => 1, 'undone_timestamp' => date('Y-m-d H:i:s') )); } elseif ($logRow->transaction_type === self::TRANSACTION_TYPE_PRODUCT_OPENED) { // Remove opened flag from corresponding log entry $stockRows = $this->Database->stock()->where('stock_id = :1 AND amount = :2 AND purchased_date = :3', $logRow->stock_id, $logRow->amount, $logRow->purchased_date)->limit(1); $stockRows->update(array( 'open' => 0, 'opened_date' => null )); // Update log entry $logRow->update(array( 'undone' => 1, 'undone_timestamp' => date('Y-m-d H:i:s') )); } elseif ($logRow->transaction_type === self::TRANSACTION_TYPE_STOCK_EDIT_NEW) { // Update log entry, no action needed $logRow->update(array( 'undone' => 1, 'undone_timestamp' => date('Y-m-d H:i:s') )); } elseif ($logRow->transaction_type === self::TRANSACTION_TYPE_STOCK_EDIT_OLD) { // Make sure there is a stock row still $stockRow = $this->Database->stock()->where('id = :1', $logRow->stock_row_id)->fetch(); if ($stockRow == null) { throw new \Exception('Booking does not exist or was already undone'); } $stockRow->update(array( 'amount' => $logRow->amount, 'best_before_date' => $logRow->best_before_date, 'purchased_date' => $logRow->purchased_date, 'price' => $logRow->price, 'location_id' => $logRow->location_id )); // Update log entry $logRow->update(array( 'undone' => 1, 'undone_timestamp' => date('Y-m-d H:i:s') )); } else { throw new \Exception('This booking cannot be undone'); } } public function UndoTransaction($transactionId) { $transactionBookings = $this->Database->stock_log()->where('undone = 0 AND transaction_id = :1', $transactionId)->orderBy('id', 'DESC')->fetchAll(); if (count($transactionBookings) === 0) { throw new \Exception('This transaction was not found or already undone'); } foreach ($transactionBookings as $transactionBooking) { $this->UndoBooking($transactionBooking->id, true); } } }