2017-04-16 23:11:03 +02:00
< ? php
2018-04-11 19:49:35 +02:00
namespace Grocy\Services ;
class StockService extends BaseService
2017-04-16 23:11:03 +02:00
{
2017-04-19 21:09:28 +02:00
const TRANSACTION_TYPE_PURCHASE = 'purchase' ;
const TRANSACTION_TYPE_CONSUME = 'consume' ;
const TRANSACTION_TYPE_INVENTORY_CORRECTION = 'inventory-correction' ;
2018-11-17 19:39:37 +01:00
const TRANSACTION_TYPE_PRODUCT_OPENED = 'product-opened' ;
2017-04-19 21:09:28 +02:00
2018-10-02 17:06:21 +02:00
public function GetCurrentStock ( $includeNotInStockButMissingProducts = false )
2017-04-16 23:11:03 +02:00
{
2019-09-19 18:11:03 +02:00
$missingProductsView = 'stock_missing_products_including_opened' ;
if ( ! GROCY_FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT )
{
$missingProductsView = 'stock_missing_products' ;
}
$sql = 'SELECT * FROM stock_current UNION SELECT id, 0, 0, null, 0, 0, 0 FROM ' . $missingProductsView . ' WHERE id NOT IN (SELECT product_id FROM stock_current)' ;
2018-10-02 17:06:21 +02:00
if ( $includeNotInStockButMissingProducts )
{
2019-09-19 18:11:03 +02:00
$sql = 'SELECT * FROM stock_current WHERE best_before_date IS NOT NULL UNION SELECT id, 0, 0, null, 0, 0, 0 FROM ' . $missingProductsView . ' WHERE id NOT IN (SELECT product_id FROM stock_current)' ;
2018-10-02 17:06:21 +02:00
}
2018-04-11 19:49:35 +02:00
return $this -> DatabaseService -> ExecuteDbQuery ( $sql ) -> fetchAll ( \PDO :: FETCH_OBJ );
2017-04-21 13:21:09 +02:00
}
2019-08-10 16:34:29 +02:00
public function GetCurrentStockLocationContent ()
{
$sql = 'SELECT * FROM stock_current_location_content' ;
return $this -> DatabaseService -> ExecuteDbQuery ( $sql ) -> fetchAll ( \PDO :: FETCH_OBJ );
}
2019-03-01 20:25:01 +01:00
public function GetCurrentStockLocations ()
{
$sql = 'SELECT * FROM stock_current_locations' ;
return $this -> DatabaseService -> ExecuteDbQuery ( $sql ) -> fetchAll ( \PDO :: FETCH_OBJ );
}
2019-03-03 16:33:48 +01:00
public function GetCurrentProductPrices ()
{
$sql = 'SELECT * FROM products_current_price' ;
return $this -> DatabaseService -> ExecuteDbQuery ( $sql ) -> fetchAll ( \PDO :: FETCH_OBJ );
}
2018-04-11 19:49:35 +02:00
public function GetMissingProducts ()
2017-04-21 13:21:09 +02:00
{
2019-09-19 18:11:03 +02:00
$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' ;
}
2018-04-11 19:49:35 +02:00
return $this -> DatabaseService -> ExecuteDbQuery ( $sql ) -> fetchAll ( \PDO :: FETCH_OBJ );
2017-04-16 23:11:03 +02:00
}
2019-03-10 12:20:31 +01:00
public function GetProductIdFromBarcode ( string $barcode )
{
2019-03-10 14:53:06 +01:00
$potentialProduct = $this -> Database -> products () -> where ( " ',' || barcode || ',' LIKE '%,' || :1 || ',%' AND IFNULL(barcode, '') != '' " , $barcode ) -> limit ( 1 ) -> fetch ();
2019-03-10 12:20:31 +01:00
2019-03-10 13:43:58 +01:00
if ( $potentialProduct === null )
2019-03-10 12:20:31 +01:00
{
2019-03-10 13:43:58 +01:00
throw new \Exception ( " No product with barcode $barcode found " );
2019-03-10 12:20:31 +01:00
}
2019-03-10 13:43:58 +01:00
return intval ( $potentialProduct -> id );
2019-03-10 12:20:31 +01:00
}
2018-10-02 17:06:21 +02:00
public function GetExpiringProducts ( int $days = 5 , bool $excludeExpired = false )
2018-08-04 14:25:32 +02:00
{
2018-10-02 17:06:21 +02:00
$currentStock = $this -> GetCurrentStock ( true );
2019-04-05 18:41:21 +02:00
$currentStock = FindAllObjectsInArrayByPropertyValue ( $currentStock , 'best_before_date' , date ( 'Y-m-d 23:59:59' , strtotime ( " + $days days " )), '<' );
2018-10-02 17:06:21 +02:00
if ( $excludeExpired )
{
2019-04-05 18:41:21 +02:00
$currentStock = FindAllObjectsInArrayByPropertyValue ( $currentStock , 'best_before_date' , date ( 'Y-m-d 23:59:59' , strtotime ( 'now' )), '>' );
2018-10-02 17:06:21 +02:00
}
return $currentStock ;
2018-08-04 14:25:32 +02:00
}
2018-04-11 19:49:35 +02:00
public function GetProductDetails ( int $productId )
2017-04-16 23:11:03 +02:00
{
2018-04-22 14:25:08 +02:00
if ( ! $this -> ProductExists ( $productId ))
{
throw new \Exception ( 'Product does not exist' );
}
2019-09-14 17:34:36 +02:00
$stockCurrentRow = FindObjectinArrayByPropertyValue ( $this -> GetCurrentStock (), 'product_id' , $productId );
2019-09-19 12:48:02 +02:00
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 ;
}
2018-04-11 19:49:35 +02:00
$product = $this -> Database -> products ( $productId );
2018-10-27 17:26:00 +02:00
$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' );
2018-08-07 20:11:08 +02:00
$nextBestBeforeDate = $this -> Database -> stock () -> where ( 'product_id' , $productId ) -> min ( 'best_before_date' );
2018-04-11 19:49:35 +02:00
$quPurchase = $this -> Database -> quantity_units ( $product -> qu_id_purchase );
$quStock = $this -> Database -> quantity_units ( $product -> qu_id_stock );
2019-03-04 17:43:12 +01:00
$location = $this -> Database -> locations ( $product -> location_id );
2019-04-22 10:11:58 +02:00
$averageShelfLifeDays = intval ( $this -> Database -> stock_average_product_shelf_life () -> where ( 'id' , $productId ) -> fetch () -> average_shelf_life_days );
2018-08-04 07:45:24 +02:00
$lastPrice = null ;
2019-07-07 09:25:13 +02:00
$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 ();
2018-08-04 07:45:24 +02:00
if ( $lastLogRow !== null && ! empty ( $lastLogRow ))
{
$lastPrice = $lastLogRow -> price ;
}
2017-04-16 23:11:03 +02:00
2019-09-21 13:30:44 +02:00
$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 ;
2019-04-22 10:11:58 +02:00
$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 ;
2017-04-16 23:11:03 +02:00
return array (
'product' => $product ,
'last_purchased' => $productLastPurchased ,
'last_used' => $productLastUsed ,
2019-09-14 17:34:36 +02:00
'stock_amount' => $stockCurrentRow -> amount ,
'stock_amount_opened' => $stockCurrentRow -> amount_opened ,
'stock_amount_aggregated' => $stockCurrentRow -> amount_aggregated ,
'stock_amount_opened_aggregated' => $stockCurrentRow -> amount_opened_aggregated ,
2017-04-16 23:11:03 +02:00
'quantity_unit_purchase' => $quPurchase ,
2018-07-26 20:27:38 +02:00
'quantity_unit_stock' => $quStock ,
2018-08-07 20:11:08 +02:00
'last_price' => $lastPrice ,
2019-03-04 17:43:12 +01:00
'next_best_before_date' => $nextBestBeforeDate ,
2019-04-22 10:11:58 +02:00
'location' => $location ,
'average_shelf_life_days' => $averageShelfLifeDays ,
2019-09-14 17:34:36 +02:00
'spoil_rate_percent' => $spoilRate ,
'is_aggregated_amount' => $stockCurrentRow -> is_aggregated_amount
2017-04-16 23:11:03 +02:00
);
}
2018-07-26 20:27:38 +02:00
public function GetProductPriceHistory ( int $productId )
{
if ( ! $this -> ProductExists ( $productId ))
{
throw new \Exception ( 'Product does not exist' );
}
$returnData = array ();
2019-07-07 09:25:13 +02:00
$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' );
2018-07-26 20:27:38 +02:00
foreach ( $rows as $row )
{
$returnData [] = array (
'date' => $row -> purchased_date ,
'price' => $row -> price
);
}
return $returnData ;
}
2018-11-17 19:39:37 +01:00
public function GetProductStockEntries ( $productId , $excludeOpened = false )
2018-11-17 17:51:35 +01:00
{
// In order of next use:
// First expiring first, then first in first out
2018-11-17 19:39:37 +01:00
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 ();
}
2018-11-17 17:51:35 +01:00
}
2019-08-10 08:20:52 +02:00
public function AddProduct ( int $productId , float $amount , $bestBeforeDate , $transactionType , $purchasedDate , $price , $locationId = null )
2017-04-19 21:09:28 +02:00
{
2018-04-22 14:25:08 +02:00
if ( ! $this -> ProductExists ( $productId ))
{
throw new \Exception ( 'Product does not exist' );
}
2019-03-05 17:51:50 +01:00
// 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 )
{
2019-09-19 21:10:36 +02:00
if ( $amount <= floatval ( $productDetails -> product -> tare_weight ) + floatval ( $productDetails -> stock_amount ))
2019-03-05 17:51:50 +01:00
{
throw new \Exception ( 'The amount cannot be lower or equal than the defined tare weight + current stock amount' );
}
2019-09-19 21:10:36 +02:00
$amount = $amount - floatval ( $productDetails -> stock_amount ) - floatval ( $productDetails -> product -> tare_weight );
2019-03-05 17:51:50 +01:00
}
2019-08-04 20:58:11 +02:00
//Sets the default best before date, if none is supplied
if ( $bestBeforeDate == null )
{
2019-08-10 08:20:52 +02:00
if ( intval ( $productDetails -> product -> default_best_before_days ) == - 1 )
{
2019-08-04 20:58:11 +02:00
$bestBeforeDate = date ( '2999-12-31' );
2019-08-10 08:20:52 +02:00
}
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' );
2019-08-04 20:58:11 +02:00
}
}
2019-03-05 17:51:50 +01:00
2018-11-18 12:34:05 +01:00
if ( $transactionType === self :: TRANSACTION_TYPE_PURCHASE || $transactionType === self :: TRANSACTION_TYPE_INVENTORY_CORRECTION )
2017-04-20 17:10:21 +02:00
{
$stockId = uniqid ();
2018-04-11 19:49:35 +02:00
$logRow = $this -> Database -> stock_log () -> createRow ( array (
2017-04-20 17:10:21 +02:00
'product_id' => $productId ,
'amount' => $amount ,
'best_before_date' => $bestBeforeDate ,
2018-07-26 20:27:38 +02:00
'purchased_date' => $purchasedDate ,
2017-04-20 17:10:21 +02:00
'stock_id' => $stockId ,
2018-07-26 20:27:38 +02:00
'transaction_type' => $transactionType ,
2019-03-01 20:25:01 +01:00
'price' => $price ,
'location_id' => $locationId
2017-04-20 17:10:21 +02:00
));
$logRow -> save ();
2018-10-27 17:26:00 +02:00
$returnValue = $this -> Database -> lastInsertId ();
2018-04-11 19:49:35 +02:00
$stockRow = $this -> Database -> stock () -> createRow ( array (
2017-04-20 17:10:21 +02:00
'product_id' => $productId ,
'amount' => $amount ,
'best_before_date' => $bestBeforeDate ,
2018-07-26 20:27:38 +02:00
'purchased_date' => $purchasedDate ,
2017-04-20 17:10:21 +02:00
'stock_id' => $stockId ,
2019-03-01 20:25:01 +01:00
'price' => $price ,
'location_id' => $locationId
2017-04-20 17:10:21 +02:00
));
$stockRow -> save ();
2018-10-27 17:26:00 +02:00
return $returnValue ;
2017-04-20 17:10:21 +02:00
}
else
{
2018-04-22 14:25:08 +02:00
throw new \Exception ( " Transaction type $transactionType is not valid (StockService.AddProduct) " );
2017-04-20 17:10:21 +02:00
}
2017-04-19 21:09:28 +02:00
}
2019-03-03 18:20:06 +01:00
public function ConsumeProduct ( int $productId , float $amount , bool $spoiled , $transactionType , $specificStockEntryId = 'default' , $recipeId = null )
2017-04-16 23:11:03 +02:00
{
2018-04-22 14:25:08 +02:00
if ( ! $this -> ProductExists ( $productId ))
{
throw new \Exception ( 'Product does not exist' );
}
2019-03-05 17:51:50 +01:00
// 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 )
{
2019-09-19 21:10:36 +02:00
if ( $amount < floatval ( $productDetails -> product -> tare_weight ))
2019-03-05 17:51:50 +01:00
{
throw new \Exception ( 'The amount cannot be lower than the defined tare weight' );
}
2019-09-19 21:10:36 +02:00
$amount = abs ( $amount - floatval ( $productDetails -> stock_amount ) - floatval ( $productDetails -> product -> tare_weight ));
2019-03-05 17:51:50 +01:00
}
2018-11-18 12:34:05 +01:00
if ( $transactionType === self :: TRANSACTION_TYPE_CONSUME || $transactionType === self :: TRANSACTION_TYPE_INVENTORY_CORRECTION )
2017-04-16 23:11:03 +02:00
{
2018-04-11 19:49:35 +02:00
$productStockAmount = $this -> Database -> stock () -> where ( 'product_id' , $productId ) -> sum ( 'amount' );
2018-11-17 17:51:35 +01:00
$potentialStockEntries = $this -> GetProductStockEntries ( $productId );
2017-04-16 23:11:03 +02:00
2017-04-20 17:10:21 +02:00
if ( $amount > $productStockAmount )
2017-04-16 23:11:03 +02:00
{
2019-03-10 13:50:28 +01:00
throw new \Exception ( 'Amount to be consumed cannot be > current stock amount' );
2017-04-16 23:11:03 +02:00
}
2017-04-20 17:10:21 +02:00
2018-11-17 17:51:35 +01:00
if ( $specificStockEntryId !== 'default' )
{
$potentialStockEntries = FindAllObjectsInArrayByPropertyValue ( $potentialStockEntries , 'stock_id' , $specificStockEntryId );
}
2017-04-20 17:10:21 +02:00
foreach ( $potentialStockEntries as $stockEntry )
2017-04-16 23:11:03 +02:00
{
2017-04-20 17:10:21 +02:00
if ( $amount == 0 )
{
break ;
}
2018-11-18 12:34:05 +01:00
if ( $amount >= $stockEntry -> amount ) // Take the whole stock entry
2017-04-20 17:10:21 +02:00
{
2018-04-11 19:49:35 +02:00
$logRow = $this -> Database -> stock_log () -> createRow ( array (
2017-04-20 17:10:21 +02:00
'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 ,
2018-07-26 20:27:38 +02:00
'transaction_type' => $transactionType ,
2018-11-17 19:39:37 +01:00
'price' => $stockEntry -> price ,
2019-03-03 18:20:06 +01:00
'opened_date' => $stockEntry -> opened_date ,
'recipe_id' => $recipeId
2017-04-20 17:10:21 +02:00
));
$logRow -> save ();
$stockEntry -> delete ();
2018-11-18 12:34:05 +01:00
$amount -= $stockEntry -> amount ;
2017-04-20 17:10:21 +02:00
}
2018-11-18 12:34:05 +01:00
else // Stock entry amount is > than needed amount -> split the stock entry resp. update the amount
2017-04-20 17:10:21 +02:00
{
2018-11-18 12:34:05 +01:00
$restStockAmount = $stockEntry -> amount - $amount ;
2018-04-11 19:49:35 +02:00
$logRow = $this -> Database -> stock_log () -> createRow ( array (
2017-04-20 17:10:21 +02:00
'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 ,
2018-07-26 20:27:38 +02:00
'transaction_type' => $transactionType ,
2018-11-17 19:39:37 +01:00
'price' => $stockEntry -> price ,
2019-03-03 18:20:06 +01:00
'opened_date' => $stockEntry -> opened_date ,
'recipe_id' => $recipeId
2017-04-20 17:10:21 +02:00
));
$logRow -> save ();
$stockEntry -> update ( array (
'amount' => $restStockAmount
));
2018-11-18 12:34:05 +01:00
$amount = 0 ;
2017-04-20 17:10:21 +02:00
}
2017-04-16 23:11:03 +02:00
}
2017-04-20 17:10:21 +02:00
2018-10-27 17:26:00 +02:00
return $this -> Database -> lastInsertId ();
2017-04-20 17:10:21 +02:00
}
else
{
2018-04-10 20:30:11 +02:00
throw new Exception ( " Transaction type $transactionType is not valid (StockService.ConsumeProduct) " );
2017-04-20 17:10:21 +02:00
}
}
2019-08-10 08:20:52 +02:00
public function InventoryProduct ( int $productId , int $newAmount , $bestBeforeDate , $locationId = null , $price = null )
2017-04-20 17:10:21 +02:00
{
2018-04-22 14:25:08 +02:00
if ( ! $this -> ProductExists ( $productId ))
{
throw new \Exception ( 'Product does not exist' );
}
2017-04-20 17:10:21 +02:00
2019-03-05 17:51:50 +01:00
$productDetails = ( object ) $this -> GetProductDetails ( $productId );
2019-05-03 22:11:20 +02:00
if ( $price === null )
{
$price = $productDetails -> last_price ;
}
2019-03-05 17:51:50 +01:00
// 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 )
{
2019-09-19 21:10:36 +02:00
$containerWeight = floatval ( $productDetails -> product -> tare_weight );
2019-03-05 17:51:50 +01:00
}
2019-09-19 21:10:36 +02:00
if ( $newAmount == floatval ( $productDetails -> stock_amount ) + $containerWeight )
2019-05-03 18:37:02 +02:00
{
throw new \Exception ( 'The new amount cannot equal the current stock amount' );
}
2019-09-19 21:10:36 +02:00
else if ( $newAmount > floatval ( $productDetails -> stock_amount ) + $containerWeight )
2017-04-20 17:10:21 +02:00
{
2019-09-19 21:10:36 +02:00
$bookingAmount = $newAmount - floatval ( $productDetails -> stock_amount );
2019-03-05 17:51:50 +01:00
if ( $productDetails -> product -> enable_tare_weight_handling == 1 )
{
$bookingAmount = $newAmount ;
}
2019-09-19 21:10:36 +02:00
return $this -> AddProduct ( $productId , $bookingAmount , $bestBeforeDate , self :: TRANSACTION_TYPE_INVENTORY_CORRECTION , date ( 'Y-m-d' ), $price , $locationId );
2017-04-20 17:10:21 +02:00
}
2019-03-05 17:51:50 +01:00
else if ( $newAmount < $productDetails -> stock_amount + $containerWeight )
2017-04-20 17:10:21 +02:00
{
2019-03-05 17:51:50 +01:00
$bookingAmount = $productDetails -> stock_amount - $newAmount ;
if ( $productDetails -> product -> enable_tare_weight_handling == 1 )
{
$bookingAmount = $newAmount ;
}
2019-09-19 21:10:36 +02:00
return $this -> ConsumeProduct ( $productId , $bookingAmount , false , self :: TRANSACTION_TYPE_INVENTORY_CORRECTION );
2017-04-16 23:11:03 +02:00
}
2019-09-19 21:10:36 +02:00
return null ;
2017-04-16 23:11:03 +02:00
}
2017-04-21 15:36:04 +02:00
2019-01-26 14:17:02 +01:00
public function OpenProduct ( int $productId , float $amount , $specificStockEntryId = 'default' )
2018-11-17 19:39:37 +01:00
{
if ( ! $this -> ProductExists ( $productId ))
{
throw new \Exception ( 'Product does not exist' );
}
2018-11-18 13:35:21 +01:00
$productStockAmountUnopened = $this -> Database -> stock () -> where ( 'product_id = :1 AND open = 0' , $productId ) -> sum ( 'amount' );
2018-11-17 19:39:37 +01:00
$potentialStockEntries = $this -> GetProductStockEntries ( $productId , true );
$product = $this -> Database -> products ( $productId );
2018-11-18 13:35:21 +01:00
if ( $amount > $productStockAmountUnopened )
2018-11-17 19:39:37 +01:00
{
2019-03-10 13:50:28 +01:00
throw new \Exception ( 'Amount to be opened cannot be > current unopened stock amount' );
2018-11-17 19:39:37 +01:00
}
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 )
{
2018-11-18 12:34:05 +01:00
$newBestBeforeDate = date ( " Y-m-d " , strtotime ( '+' . $product -> default_best_before_days_after_open . ' days' ));
2018-11-17 19:39:37 +01:00
}
2018-11-18 12:34:05 +01:00
if ( $amount >= $stockEntry -> amount ) // Mark the whole stock entry as opened
2018-11-17 19:39:37 +01:00
{
$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
));
2018-11-18 12:34:05 +01:00
$amount -= $stockEntry -> amount ;
2018-11-17 19:39:37 +01:00
}
2018-11-18 12:34:05 +01:00
else // Stock entry amount is > than needed amount -> split the stock entry
2018-11-17 19:39:37 +01:00
{
2018-11-18 12:34:05 +01:00
$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 ();
2018-11-17 19:39:37 +01:00
$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 (
2018-11-18 12:34:05 +01:00
'amount' => $amount ,
2018-11-17 19:39:37 +01:00
'open' => 1 ,
'opened_date' => date ( 'Y-m-d' ),
'best_before_date' => $newBestBeforeDate
));
2018-11-18 12:34:05 +01:00
$amount = 0 ;
2018-11-17 19:39:37 +01:00
}
}
return $this -> Database -> lastInsertId ();
}
2019-04-20 17:04:40 +02:00
public function AddMissingProductsToShoppingList ( $listId = 1 )
2017-04-21 15:36:04 +02:00
{
2019-04-22 08:21:57 +02:00
if ( ! $this -> ShoppingListExists ( $listId ))
{
throw new \Exception ( 'Shopping list does not exist' );
}
2018-04-11 19:49:35 +02:00
$missingProducts = $this -> GetMissingProducts ();
2017-04-21 15:36:04 +02:00
foreach ( $missingProducts as $missingProduct )
{
2018-04-11 19:49:35 +02:00
$product = $this -> Database -> products () -> where ( 'id' , $missingProduct -> id ) -> fetch ();
2019-01-26 13:09:01 +01:00
$amountToAdd = ceil ( $missingProduct -> amount_missing / $product -> qu_factor_purchase_to_stock );
2017-04-21 15:36:04 +02:00
2018-04-11 19:49:35 +02:00
$alreadyExistingEntry = $this -> Database -> shopping_list () -> where ( 'product_id' , $missingProduct -> id ) -> fetch ();
2018-11-18 12:34:05 +01:00
if ( $alreadyExistingEntry ) // Update
2017-04-21 15:36:04 +02:00
{
2019-01-26 13:09:01 +01:00
if ( $alreadyExistingEntry -> amount < $amountToAdd )
{
$alreadyExistingEntry -> update ( array (
2019-04-20 17:04:40 +02:00
'amount' => $amountToAdd ,
'shopping_list_id' => $listId
2019-01-26 13:09:01 +01:00
));
}
2017-04-21 15:36:04 +02:00
}
2018-11-18 12:34:05 +01:00
else // Insert
2017-04-21 15:36:04 +02:00
{
2018-04-11 19:49:35 +02:00
$shoppinglistRow = $this -> Database -> shopping_list () -> createRow ( array (
2017-04-21 15:36:04 +02:00
'product_id' => $missingProduct -> id ,
2019-04-20 17:04:40 +02:00
'amount' => $amountToAdd ,
'shopping_list_id' => $listId
2017-04-21 15:36:04 +02:00
));
$shoppinglistRow -> save ();
}
}
}
2018-04-22 14:25:08 +02:00
2019-04-20 17:04:40 +02:00
public function ClearShoppingList ( $listId = 1 )
2018-07-15 08:29:26 +02:00
{
2019-04-22 08:21:57 +02:00
if ( ! $this -> ShoppingListExists ( $listId ))
{
throw new \Exception ( 'Shopping list does not exist' );
}
2019-04-20 17:04:40 +02:00
$this -> Database -> shopping_list () -> where ( 'shopping_list_id = :1' , $listId ) -> delete ();
2018-07-15 08:29:26 +02:00
}
2019-08-04 20:31:47 +02:00
public function RemoveProductFromShoppingList ( $productId , $amount = 1 , $listId = 1 )
{
if ( ! $this -> ShoppingListExists ( $listId ))
{
throw new \Exception ( 'Shopping list does not exist' );
}
2019-08-10 08:20:52 +02:00
2019-08-04 20:31:47 +02:00
$productRow = $this -> Database -> shopping_list () -> where ( 'product_id = :1' , $productId ) -> fetch ();
2019-08-10 08:20:52 +02:00
2019-08-04 20:31:47 +02:00
//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 ();
2019-08-10 08:20:52 +02:00
}
else
{
2019-08-04 20:31:47 +02:00
$productRow -> update ( array ( 'amount' => $newAmount ));
}
}
}
2019-08-30 09:21:11 +02:00
public function AddProductToShoppingList ( $productId , $amount = 1 , $listId = 1 )
{
if ( ! $this -> ShoppingListExists ( $listId ))
{
throw new \Exception ( 'Shopping list does not exist' );
}
2019-08-31 14:08:15 +02:00
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
));
}
else // Insert
{
$shoppinglistRow = $this -> Database -> shopping_list () -> createRow ( array (
2019-09-24 15:50:35 +02:00
'product_id' => $productId ,
2019-08-31 14:08:15 +02:00
'amount' => $amount ,
'shopping_list_id' => $listId
));
$shoppinglistRow -> save ();
}
2019-08-30 09:21:11 +02:00
}
2018-04-22 14:25:08 +02:00
private function ProductExists ( $productId )
{
$productRow = $this -> Database -> products () -> where ( 'id = :1' , $productId ) -> fetch ();
return $productRow !== null ;
}
2018-04-22 19:47:46 +02:00
2019-04-22 08:21:57 +02:00
private function ShoppingListExists ( $listId )
{
$shoppingListRow = $this -> Database -> shopping_lists () -> where ( 'id = :1' , $listId ) -> fetch ();
return $shoppingListRow !== null ;
}
2018-04-22 19:47:46 +02:00
private function LoadBarcodeLookupPlugin ()
{
2018-07-24 19:41:35 +02:00
$pluginName = defined ( 'GROCY_STOCK_BARCODE_LOOKUP_PLUGIN' ) ? GROCY_STOCK_BARCODE_LOOKUP_PLUGIN : '' ;
2018-04-22 19:47:46 +02:00
if ( empty ( $pluginName ))
{
throw new \Exception ( 'No barcode lookup plugin defined' );
}
2018-07-24 19:41:35 +02:00
$path = GROCY_DATAPATH . " /plugins/ $pluginName .php " ;
2018-04-22 19:47:46 +02:00
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 ;
}
2018-10-26 22:28:58 +02:00
2018-10-27 10:19:06 +02:00
public function UndoBooking ( $bookingId )
2018-10-26 22:28:58 +02:00
{
2018-10-27 10:19:06 +02:00
$logRow = $this -> Database -> stock_log () -> where ( 'id = :1 AND undone = 0' , $bookingId ) -> fetch ();
2018-10-26 22:28:58 +02:00
if ( $logRow == null )
{
2018-10-27 10:19:06 +02:00
throw new \Exception ( 'Booking does not exist or was already undone' );
}
$hasSubsequentBookings = $this -> Database -> stock_log () -> where ( 'stock_id = :1 AND id != :2 AND id > :2' , $logRow -> stock_id , $logRow -> id ) -> count () > 0 ;
if ( $hasSubsequentBookings )
{
throw new \Exception ( 'Booking has subsequent dependent bookings, undo not possible' );
2018-10-26 22:28:58 +02:00
}
2018-10-27 10:19:06 +02:00
if ( $logRow -> transaction_type === self :: TRANSACTION_TYPE_PURCHASE || ( $logRow -> transaction_type === self :: TRANSACTION_TYPE_INVENTORY_CORRECTION && $logRow -> amount > 0 ))
2018-10-26 22:28:58 +02:00
{
// 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' )
));
}
2018-10-27 10:19:06 +02:00
elseif ( $logRow -> transaction_type === self :: TRANSACTION_TYPE_CONSUME || ( $logRow -> transaction_type === self :: TRANSACTION_TYPE_INVENTORY_CORRECTION && $logRow -> amount < 0 ))
2018-10-26 22:28:58 +02:00
{
// 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 ,
2018-11-18 12:34:05 +01:00
'price' => $logRow -> price ,
'opened_date' => $logRow -> opened_date
2018-10-26 22:28:58 +02:00
));
$stockRow -> save ();
// Update log entry
$logRow -> update ( array (
'undone' => 1 ,
'undone_timestamp' => date ( 'Y-m-d H:i:s' )
));
}
2018-11-17 19:39:37 +01:00
elseif ( $logRow -> transaction_type === self :: TRANSACTION_TYPE_PRODUCT_OPENED )
{
// Remove opened flag from corresponding log entry
2018-11-18 12:34:05 +01:00
$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 );
2018-11-17 19:39:37 +01:00
$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' )
));
}
2018-10-26 22:28:58 +02:00
else
{
throw new \Exception ( 'This booking cannot be undone' );
}
}
2017-04-16 23:11:03 +02:00
}