2017-04-16 23:11:03 +02:00
< ? php
2018-04-11 19:49:35 +02:00
namespace Grocy\Services ;
2021-06-12 17:21:12 +02:00
use Grocy\Helpers\Grocycode ;
use Grocy\Helpers\WebhookRunner ;
2018-04-11 19:49:35 +02:00
class StockService extends BaseService
2017-04-16 23:11:03 +02:00
{
2017-04-19 21:09:28 +02:00
const TRANSACTION_TYPE_CONSUME = 'consume' ;
2021-07-03 17:46:47 +02:00
2020-08-31 20:40:31 +02:00
const TRANSACTION_TYPE_INVENTORY_CORRECTION = 'inventory-correction' ;
2021-07-03 17:46:47 +02:00
2020-08-31 20:40:31 +02:00
const TRANSACTION_TYPE_PRODUCT_OPENED = 'product-opened' ;
2021-07-03 17:46:47 +02:00
2020-08-31 20:40:31 +02:00
const TRANSACTION_TYPE_PURCHASE = 'purchase' ;
2021-07-03 17:46:47 +02:00
2020-08-31 20:40:31 +02:00
const TRANSACTION_TYPE_SELF_PRODUCTION = 'self-production' ;
2021-07-03 17:46:47 +02:00
2020-08-31 20:40:31 +02:00
const TRANSACTION_TYPE_STOCK_EDIT_NEW = 'stock-edit-new' ;
2021-07-03 17:46:47 +02:00
2020-08-31 20:40:31 +02:00
const TRANSACTION_TYPE_STOCK_EDIT_OLD = 'stock-edit-old' ;
2021-07-03 17:46:47 +02:00
2020-08-31 20:40:31 +02:00
const TRANSACTION_TYPE_TRANSFER_FROM = 'transfer_from' ;
2021-07-03 17:46:47 +02:00
2020-08-31 20:40:31 +02:00
const TRANSACTION_TYPE_TRANSFER_TO = 'transfer_to' ;
2018-08-04 14:25:32 +02:00
2020-08-31 20:40:31 +02:00
public function AddMissingProductsToShoppingList ( $listId = 1 )
2017-04-16 23:11:03 +02:00
{
2020-08-31 20:40:31 +02:00
if ( ! $this -> ShoppingListExists ( $listId ))
2019-04-22 10:11:58 +02:00
{
2020-08-31 20:40:31 +02:00
throw new \Exception ( 'Shopping list does not exist' );
2019-04-22 10:11:58 +02:00
}
2017-04-16 23:11:03 +02:00
2020-08-31 20:40:31 +02:00
$missingProducts = $this -> GetMissingProducts ();
foreach ( $missingProducts as $missingProduct )
2018-07-26 20:27:38 +02:00
{
2020-08-31 20:40:31 +02:00
$product = $this -> getDatabase () -> products () -> where ( 'id' , $missingProduct -> id ) -> fetch ();
2020-11-13 17:30:57 +01:00
$amountToAdd = round ( $missingProduct -> amount_missing , 2 );
2018-07-26 20:27:38 +02:00
2020-08-31 20:40:31 +02:00
$alreadyExistingEntry = $this -> getDatabase () -> shopping_list () -> where ( 'product_id' , $missingProduct -> id ) -> fetch ();
2020-01-17 10:54:34 -06:00
2020-09-01 21:29:47 +02:00
if ( $alreadyExistingEntry )
2021-07-16 17:32:08 +02:00
{
// Update
2020-08-31 20:40:31 +02:00
if ( $alreadyExistingEntry -> amount < $amountToAdd )
{
$alreadyExistingEntry -> update ([
'amount' => $amountToAdd ,
'shopping_list_id' => $listId
]);
}
}
2020-09-01 21:29:47 +02:00
else
2021-07-16 17:32:08 +02:00
{
// Insert
2020-08-31 20:40:31 +02:00
$shoppinglistRow = $this -> getDatabase () -> shopping_list () -> createRow ([
'product_id' => $missingProduct -> id ,
'amount' => $amountToAdd ,
'shopping_list_id' => $listId
]);
$shoppinglistRow -> save ();
}
2018-11-17 19:39:37 +01:00
}
2019-12-19 12:48:36 -06:00
}
2020-11-15 19:53:44 +01:00
public function AddOverdueProductsToShoppingList ( $listId = 1 )
2020-10-04 15:20:34 +02:00
{
if ( ! $this -> ShoppingListExists ( $listId ))
{
throw new \Exception ( 'Shopping list does not exist' );
}
2020-11-15 19:53:44 +01:00
$overdueProducts = $this -> GetDueProducts ( - 1 );
foreach ( $overdueProducts as $overdueProduct )
2020-10-04 15:20:34 +02:00
{
2020-11-15 19:53:44 +01:00
$product = $this -> getDatabase () -> products () -> where ( 'id' , $overdueProduct -> product_id ) -> fetch ();
2020-10-04 15:20:34 +02:00
2020-11-15 19:53:44 +01:00
$alreadyExistingEntry = $this -> getDatabase () -> shopping_list () -> where ( 'product_id' , $overdueProduct -> product_id ) -> fetch ();
2020-10-04 15:20:34 +02:00
if ( ! $alreadyExistingEntry )
{
$shoppinglistRow = $this -> getDatabase () -> shopping_list () -> createRow ([
2020-11-15 19:53:44 +01:00
'product_id' => $overdueProduct -> product_id ,
2020-10-04 15:20:34 +02:00
'amount' => 1 ,
'shopping_list_id' => $listId
]);
$shoppinglistRow -> save ();
}
}
}
2020-11-15 22:38:21 +01:00
public function AddExpiredProductsToShoppingList ( $listId = 1 )
{
if ( ! $this -> ShoppingListExists ( $listId ))
{
throw new \Exception ( 'Shopping list does not exist' );
}
$expiredProducts = $this -> GetExpiredProducts ();
foreach ( $expiredProducts as $expiredProduct )
{
$product = $this -> getDatabase () -> products () -> where ( 'id' , $expiredProduct -> product_id ) -> fetch ();
$alreadyExistingEntry = $this -> getDatabase () -> shopping_list () -> where ( 'product_id' , $expiredProduct -> product_id ) -> fetch ();
if ( ! $alreadyExistingEntry )
{
$shoppinglistRow = $this -> getDatabase () -> shopping_list () -> createRow ([
'product_id' => $expiredProduct -> product_id ,
'amount' => 1 ,
'shopping_list_id' => $listId
]);
$shoppinglistRow -> save ();
}
}
}
2021-07-03 18:30:53 +02:00
public function AddProduct ( int $productId , float $amount , $bestBeforeDate , $transactionType , $purchasedDate , $price , $locationId = null , $shoppingLocationId = null , & $transactionId = null , $runWebhook = 0 , $addExactAmount = false )
2017-04-19 21:09:28 +02:00
{
2018-04-22 14:25:08 +02:00
if ( ! $this -> ProductExists ( $productId ))
{
2020-08-17 14:47:33 -05:00
throw new \Exception ( 'Product does not exist or is inactive' );
2018-04-22 14:25:08 +02:00
}
2020-10-18 14:09:54 +02:00
$productDetails = ( object ) $this -> GetProductDetails ( $productId );
2020-08-31 20:40:31 +02:00
2020-10-18 14:09:54 +02:00
// Tare weight handling
2020-09-01 21:29:47 +02:00
// The given amount is the new total amount including the container weight (gross)
2019-03-05 17:51:50 +01:00
// The amount to be posted needs to be the given amount - stock amount - tare weight
if ( $productDetails -> product -> enable_tare_weight_handling == 1 )
{
2021-07-03 18:30:53 +02:00
if ( $addExactAmount )
{
$amount = floatval ( $productDetails -> stock_amount ) + floatval ( $productDetails -> product -> tare_weight ) + $amount ;
}
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' );
}
2020-01-19 09:14:07 +01:00
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
}
2020-01-19 09:14:07 +01:00
2020-11-15 19:53:44 +01:00
//Set the default due date, if none is supplied
2019-08-04 20:58:11 +02:00
if ( $bestBeforeDate == null )
{
2019-08-10 08:20:52 +02:00
if ( intval ( $productDetails -> product -> default_best_before_days ) == - 1 )
{
2020-01-19 09:14:07 +01:00
$bestBeforeDate = date ( '2999-12-31' );
2019-08-10 08:20:52 +02:00
}
2020-09-01 21:29:47 +02:00
elseif ( intval ( $productDetails -> product -> default_best_before_days ) > 0 )
2019-08-10 08:20:52 +02:00
{
2020-08-31 20:40:31 +02:00
$bestBeforeDate = date ( 'Y-m-d' , strtotime ( date ( 'Y-m-d' ) . ' + ' . $productDetails -> product -> default_best_before_days . ' days' ));
2019-08-10 08:20:52 +02:00
}
else
{
$bestBeforeDate = date ( 'Y-m-d' );
2019-08-04 20:58:11 +02:00
}
}
2019-03-05 17:51:50 +01:00
2020-01-21 20:45:34 +01:00
if ( $transactionType === self :: TRANSACTION_TYPE_PURCHASE || $transactionType === self :: TRANSACTION_TYPE_INVENTORY_CORRECTION || $transactionType == self :: TRANSACTION_TYPE_SELF_PRODUCTION )
2017-04-20 17:10:21 +02:00
{
2019-12-19 12:48:36 -06:00
if ( $transactionId === null )
{
$transactionId = uniqid ();
}
2020-01-19 09:14:07 +01:00
2017-04-20 17:10:21 +02:00
$stockId = uniqid ();
2020-08-31 20:40:31 +02:00
$logRow = $this -> getDatabase () -> stock_log () -> createRow ([
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 ,
2019-12-19 12:48:36 -06:00
'location_id' => $locationId ,
2020-03-25 19:34:56 +01:00
'transaction_id' => $transactionId ,
'shopping_location_id' => $shoppingLocationId ,
2020-09-06 13:18:51 +02:00
'user_id' => GROCY_USER_ID
2020-08-31 20:40:31 +02:00
]);
2017-04-20 17:10:21 +02:00
$logRow -> save ();
2020-08-31 20:40:31 +02:00
$stockRow = $this -> getDatabase () -> stock () -> createRow ([
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 ,
2020-03-25 19:34:56 +01:00
'location_id' => $locationId ,
2021-06-12 17:21:12 +02:00
'shopping_location_id' => $shoppingLocationId
2020-08-31 20:40:31 +02:00
]);
2017-04-20 17:10:21 +02:00
$stockRow -> save ();
2021-07-13 19:29:23 +02:00
if ( GROCY_FEATURE_FLAG_LABEL_PRINTER && GROCY_LABEL_PRINTER_RUN_SERVER && $runWebhook )
2021-06-12 17:21:12 +02:00
{
$reps = 1 ;
if ( $runWebhook == 2 )
2021-07-16 17:32:08 +02:00
{
// 2 == run $amount times
2021-06-12 17:21:12 +02:00
$reps = intval ( floor ( $amount ));
}
$webhookData = array_merge ([
'product' => $productDetails -> product -> name ,
'grocycode' => ( string )( new Grocycode ( Grocycode :: PRODUCT , $productId , [ $stockId ])),
], GROCY_LABEL_PRINTER_PARAMS );
if ( GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING )
{
2021-07-13 19:29:23 +02:00
$webhookData [ 'due_date' ] = $this -> getLocalizationService () -> __t ( 'DD' ) . ': ' . $bestBeforeDate ;
2021-06-12 17:21:12 +02:00
}
$runner = new WebhookRunner ();
for ( $i = 0 ; $i < $reps ; $i ++ )
{
$runner -> run ( GROCY_LABEL_PRINTER_WEBHOOK , $webhookData , GROCY_LABEL_PRINTER_HOOK_JSON );
}
}
2021-07-11 21:06:05 +02:00
$this -> CompactStockEntries ( $productId );
2020-12-20 14:43:07 +01:00
return $transactionId ;
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
}
2020-08-31 20:40:31 +02:00
}
2021-07-02 17:37:06 +02:00
public function AddProductToShoppingList ( $productId , $amount = 1 , $quId = - 1 , $note = null , $listId = 1 )
2020-08-31 20:40:31 +02:00
{
if ( ! $this -> ShoppingListExists ( $listId ))
{
throw new \Exception ( 'Shopping list does not exist' );
}
if ( ! $this -> ProductExists ( $productId ))
{
throw new \Exception ( 'Product does not exist or is inactive' );
}
2021-07-02 17:37:06 +02:00
if ( $quId == - 1 )
{
$quId = $this -> getDatabase () -> products ( $productId ) -> qu_id_purchase ;
}
2020-08-31 20:40:31 +02:00
$alreadyExistingEntry = $this -> getDatabase () -> shopping_list () -> where ( 'product_id = :1 AND shopping_list_id = :2' , $productId , $listId ) -> fetch ();
2020-09-01 21:29:47 +02:00
if ( $alreadyExistingEntry )
2021-07-02 17:37:06 +02:00
{
// Update
2020-08-31 20:40:31 +02:00
$alreadyExistingEntry -> update ([
'amount' => ( $alreadyExistingEntry -> amount + $amount ),
'shopping_list_id' => $listId ,
'note' => $note
]);
}
2020-09-01 21:29:47 +02:00
else
2021-07-02 17:37:06 +02:00
{
// Insert
2020-08-31 20:40:31 +02:00
$shoppinglistRow = $this -> getDatabase () -> shopping_list () -> createRow ([
'product_id' => $productId ,
'amount' => $amount ,
2021-07-02 17:37:06 +02:00
'qu_id' => $quId ,
2020-08-31 20:40:31 +02:00
'shopping_list_id' => $listId ,
'note' => $note
]);
$shoppinglistRow -> save ();
}
}
public function ClearShoppingList ( $listId = 1 )
{
if ( ! $this -> ShoppingListExists ( $listId ))
{
throw new \Exception ( 'Shopping list does not exist' );
}
$this -> getDatabase () -> shopping_list () -> where ( 'shopping_list_id = :1' , $listId ) -> delete ();
2017-04-19 21:09:28 +02:00
}
2020-10-14 17:48:37 +02:00
public function ConsumeProduct ( int $productId , float $amount , bool $spoiled , $transactionType , $specificStockEntryId = 'default' , $recipeId = null , $locationId = null , & $transactionId = null , $allowSubproductSubstitution = false , $consumeExactAmount = false )
2017-04-16 23:11:03 +02:00
{
2018-04-22 14:25:08 +02:00
if ( ! $this -> ProductExists ( $productId ))
{
2020-08-17 14:47:33 -05:00
throw new \Exception ( 'Product does not exist or is inactive' );
2018-04-22 14:25:08 +02:00
}
2020-01-27 22:14:11 +01:00
if ( $locationId !== null && ! $this -> LocationExists ( $locationId ))
2019-12-19 12:48:36 -06:00
{
throw new \Exception ( 'Location does not exist' );
}
2021-06-12 17:21:12 +02:00
$productDetails = ( object ) $this -> GetProductDetails ( $productId );
2020-08-31 20:40:31 +02:00
2020-12-07 19:48:33 +01:00
// Tare weight handling
2020-09-01 21:29:47 +02:00
// The given amount is the new total amount including the container weight (gross)
2019-03-05 17:51:50 +01:00
// The amount to be posted needs to be the absolute value of the given amount - stock amount - tare weight
if ( $productDetails -> product -> enable_tare_weight_handling == 1 )
{
2020-10-18 14:09:54 +02:00
if ( $consumeExactAmount )
2020-10-14 17:48:37 +02:00
{
$amount = floatval ( $productDetails -> stock_amount ) + floatval ( $productDetails -> product -> tare_weight ) - $amount ;
}
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' );
}
2020-01-19 09:14:07 +01:00
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
{
2020-09-01 21:29:47 +02:00
if ( $locationId === null )
2020-12-07 19:48:33 +01:00
{
// Consume from any location
2020-01-27 22:14:11 +01:00
$potentialStockEntries = $this -> GetProductStockEntries ( $productId , false , $allowSubproductSubstitution );
2019-12-19 12:48:36 -06:00
}
2020-09-01 21:29:47 +02:00
else
2020-12-07 19:48:33 +01:00
{
// Consume only from the supplied location
2020-01-27 22:14:11 +01:00
$potentialStockEntries = $this -> GetProductStockEntriesForLocation ( $productId , $locationId , false , $allowSubproductSubstitution );
2019-12-19 12:48:36 -06:00
}
2017-04-16 23:11:03 +02:00
2020-10-14 17:48:37 +02:00
if ( $specificStockEntryId !== 'default' )
{
$potentialStockEntries = FindAllObjectsInArrayByPropertyValue ( $potentialStockEntries , 'stock_id' , $specificStockEntryId );
}
2021-01-01 13:27:57 +01:00
// TODO: This check doesn't really check against products only at the given location
// (as GetProductDetails returns the stock_amount_aggregated of all locations)
// However, $potentialStockEntries are filtered accordingly, so this currently isn't really a problem at the end
2021-06-12 17:21:12 +02:00
$productStockAmount = (( object ) $this -> GetProductDetails ( $productId )) -> stock_amount_aggregated ;
2017-04-20 17:10:21 +02:00
if ( $amount > $productStockAmount )
2017-04-16 23:11:03 +02:00
{
2019-12-19 12:48:36 -06:00
throw new \Exception ( 'Amount to be consumed cannot be > current stock amount (if supplied, at the desired location)' );
2017-04-16 23:11:03 +02:00
}
2017-04-20 17:10:21 +02:00
2019-12-19 12:48:36 -06:00
if ( $transactionId === null )
{
$transactionId = uniqid ();
}
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 ;
}
2020-12-07 19:48:33 +01:00
if ( $allowSubproductSubstitution && $stockEntry -> product_id != $productId )
{
// A sub product will be used -> use QU conversions
$subProduct = $this -> getDatabase () -> products ( $stockEntry -> product_id );
$conversion = $this -> getDatabase () -> quantity_unit_conversions_resolved () -> where ( 'product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3' , $stockEntry -> product_id , $productDetails -> product -> qu_id_stock , $subProduct -> qu_id_stock ) -> fetch ();
if ( $conversion != null )
{
$amount = $amount * floatval ( $conversion -> factor );
}
}
2020-09-01 21:29:47 +02:00
if ( $amount >= $stockEntry -> amount )
2020-12-07 19:48:33 +01:00
{
// Take the whole stock entry
2020-08-31 20:40:31 +02:00
$logRow = $this -> getDatabase () -> stock_log () -> createRow ([
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 ,
2019-12-19 12:48:36 -06:00
'recipe_id' => $recipeId ,
2020-09-06 13:18:51 +02:00
'transaction_id' => $transactionId ,
'user_id' => GROCY_USER_ID
2020-08-31 20:40:31 +02:00
]);
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
}
2020-09-01 21:29:47 +02:00
else
2020-12-07 19:48:33 +01:00
{
// Stock entry amount is > than needed amount -> split the stock entry resp. update the amount
2018-11-18 12:34:05 +01:00
$restStockAmount = $stockEntry -> amount - $amount ;
2020-08-31 20:40:31 +02:00
$logRow = $this -> getDatabase () -> stock_log () -> createRow ([
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 ,
2019-12-19 12:48:36 -06:00
'recipe_id' => $recipeId ,
2020-09-06 13:18:51 +02:00
'transaction_id' => $transactionId ,
'user_id' => GROCY_USER_ID
2020-08-31 20:40:31 +02:00
]);
2017-04-20 17:10:21 +02:00
$logRow -> save ();
2020-08-31 20:40:31 +02:00
$stockEntry -> update ([
2017-04-20 17:10:21 +02:00
'amount' => $restStockAmount
2020-08-31 20:40:31 +02:00
]);
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
2021-07-11 21:06:05 +02:00
$this -> CompactStockEntries ( $productId );
2020-12-20 14:43:07 +01:00
return $transactionId ;
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
}
}
2020-11-10 18:11:33 +01:00
public function EditStockEntry ( int $stockRowId , float $amount , $bestBeforeDate , $locationId , $shoppingLocationId , $price , $open , $purchasedDate )
2019-12-19 12:48:36 -06:00
{
2020-08-31 20:40:31 +02:00
$stockRow = $this -> getDatabase () -> stock () -> where ( 'id = :1' , $stockRowId ) -> fetch ();
if ( $stockRow === null )
2019-12-19 12:48:36 -06:00
{
2020-08-31 20:40:31 +02:00
throw new \Exception ( 'Stock does not exist' );
2019-12-19 12:48:36 -06:00
}
2020-08-31 20:40:31 +02:00
$correlationId = uniqid ();
$transactionId = uniqid ();
$logOldRowForStockUpdate = $this -> getDatabase () -> stock_log () -> createRow ([
'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 ,
'shopping_location_id' => $stockRow -> shopping_location_id ,
'correlation_id' => $correlationId ,
'transaction_id' => $transactionId ,
2020-09-06 13:18:51 +02:00
'stock_row_id' => $stockRow -> id ,
'user_id' => GROCY_USER_ID
2020-08-31 20:40:31 +02:00
]);
$logOldRowForStockUpdate -> save ();
$openedDate = $stockRow -> opened_date ;
2021-01-12 18:04:20 +01:00
if ( boolval ( $open ) && $openedDate == null )
2019-12-19 12:48:36 -06:00
{
2020-08-31 20:40:31 +02:00
$openedDate = date ( 'Y-m-d' );
2019-12-19 12:48:36 -06:00
}
2021-01-12 18:04:20 +01:00
elseif ( ! boolval ( $open ))
2019-12-19 12:48:36 -06:00
{
2020-08-31 20:40:31 +02:00
$openedDate = null ;
2019-12-19 12:48:36 -06:00
}
2020-08-31 20:40:31 +02:00
$stockRow -> update ([
'amount' => $amount ,
'price' => $price ,
'best_before_date' => $bestBeforeDate ,
'location_id' => $locationId ,
'shopping_location_id' => $shoppingLocationId ,
'opened_date' => $openedDate ,
2021-01-12 18:04:20 +01:00
'open' => BoolToInt ( $open ),
2020-08-31 20:40:31 +02:00
'purchased_date' => $purchasedDate
]);
2019-12-19 12:48:36 -06:00
2020-08-31 20:40:31 +02:00
$logNewRowForStockUpdate = $this -> getDatabase () -> stock_log () -> createRow ([
'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 ,
'shopping_location_id' => $shoppingLocationId ,
'correlation_id' => $correlationId ,
'transaction_id' => $transactionId ,
2020-09-06 13:18:51 +02:00
'stock_row_id' => $stockRow -> id ,
'user_id' => GROCY_USER_ID
2020-08-31 20:40:31 +02:00
]);
$logNewRowForStockUpdate -> save ();
2021-07-11 21:06:05 +02:00
$this -> CompactStockEntries ( $stockRow -> product_id );
2020-12-20 14:43:07 +01:00
return $transactionId ;
2020-08-31 20:40:31 +02:00
}
public function ExternalBarcodeLookup ( $barcode , $addFoundProduct )
{
$plugin = $this -> LoadBarcodeLookupPlugin ();
$pluginOutput = $plugin -> Lookup ( $barcode );
2020-09-01 21:29:47 +02:00
if ( $pluginOutput !== null )
2021-07-16 17:32:08 +02:00
{
// Lookup was successful
2020-08-31 20:40:31 +02:00
if ( $addFoundProduct === true )
2019-12-19 12:48:36 -06:00
{
2020-08-31 20:40:31 +02:00
// Add product to database and include new product id in output
$newRow = $this -> getDatabase () -> products () -> createRow ( $pluginOutput );
$newRow -> save ();
$pluginOutput [ 'id' ] = $newRow -> id ;
2019-12-19 12:48:36 -06:00
}
}
2020-08-31 20:40:31 +02:00
return $pluginOutput ;
}
2019-12-19 12:48:36 -06:00
2020-08-31 20:40:31 +02:00
public function GetCurrentStock ( $includeNotInStockButMissingProducts = false )
{
$sql = 'SELECT * FROM stock_current' ;
if ( $includeNotInStockButMissingProducts )
2019-12-19 12:48:36 -06:00
{
2020-08-31 20:40:31 +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 WHERE best_before_date IS NOT NULL UNION SELECT id, 0, 0, 0, 0, null, 0, 0, 0 FROM ' . $missingProductsView . ' WHERE id NOT IN (SELECT product_id FROM stock_current)' ;
2019-12-19 12:48:36 -06:00
}
2020-08-31 20:40:31 +02:00
$currentStockMapped = $this -> getDatabaseService () -> ExecuteDbQuery ( $sql ) -> fetchAll ( \PDO :: FETCH_GROUP | \PDO :: FETCH_OBJ );
$relevantProducts = $this -> getDatabase () -> products () -> where ( 'id IN (SELECT product_id FROM (' . $sql . ') x)' );
foreach ( $relevantProducts as $product )
2019-12-19 12:48:36 -06:00
{
2020-08-31 20:40:31 +02:00
$currentStockMapped [ $product -> id ][ 0 ] -> product_id = $product -> id ;
$currentStockMapped [ $product -> id ][ 0 ] -> product = $product ;
2019-12-19 12:48:36 -06:00
}
2020-08-31 20:40:31 +02:00
return array_column ( $currentStockMapped , 0 );
}
public function GetCurrentStockLocationContent ()
{
$sql = 'SELECT sclc.* FROM stock_current_location_content sclc JOIN products p ON sclc.product_id = p.id ORDER BY p.name' ;
return $this -> getDatabaseService () -> ExecuteDbQuery ( $sql ) -> fetchAll ( \PDO :: FETCH_OBJ );
}
public function GetCurrentStockLocations ()
{
$sql = 'SELECT * FROM stock_current_locations' ;
return $this -> getDatabaseService () -> ExecuteDbQuery ( $sql ) -> fetchAll ( \PDO :: FETCH_OBJ );
}
public function GetCurrentStockOverview ()
{
if ( ! GROCY_FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT )
2019-12-19 12:48:36 -06:00
{
2020-08-31 20:40:31 +02:00
return $this -> getDatabase () -> uihelper_stock_current_overview ();
}
else
{
return $this -> getDatabase () -> uihelper_stock_current_overview_including_opened ();
2019-12-19 12:48:36 -06:00
}
2020-08-31 20:40:31 +02:00
}
2020-11-15 19:53:44 +01:00
public function GetDueProducts ( int $days = 5 , bool $excludeOverdue = false )
2020-08-31 20:40:31 +02:00
{
$currentStock = $this -> GetCurrentStock ( false );
$currentStock = FindAllObjectsInArrayByPropertyValue ( $currentStock , 'best_before_date' , date ( 'Y-m-d 23:59:59' , strtotime ( " + $days days " )), '<' );
2020-11-15 19:53:44 +01:00
if ( $excludeOverdue )
2019-12-19 12:48:36 -06:00
{
2020-08-31 20:40:31 +02:00
$currentStock = FindAllObjectsInArrayByPropertyValue ( $currentStock , 'best_before_date' , date ( 'Y-m-d 23:59:59' , strtotime ( '-1 days' )), '>' );
}
2019-12-19 12:48:36 -06:00
2020-08-31 20:40:31 +02:00
return $currentStock ;
}
2020-01-26 20:01:30 +01:00
2020-11-15 19:53:44 +01:00
public function GetExpiredProducts ()
{
$currentStock = $this -> GetCurrentStock ( false );
2020-11-19 18:28:16 +01:00
$currentStock = FindAllObjectsInArrayByPropertyValue ( $currentStock , 'best_before_date' , date ( 'Y-m-d 23:59:59' , strtotime ( '-1 days' )), '<' );
2020-11-15 19:53:44 +01:00
$currentStock = FindAllObjectsInArrayByPropertyValue ( $currentStock , 'due_type' , 2 );
return $currentStock ;
}
2020-08-31 20:40:31 +02:00
public function GetMissingProducts ()
{
$sql = 'SELECT * FROM stock_missing_products_including_opened' ;
2020-01-26 20:01:30 +01:00
2020-08-31 20:40:31 +02:00
if ( ! GROCY_FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT )
{
$sql = 'SELECT * FROM stock_missing_products' ;
}
2020-01-26 20:01:30 +01:00
2020-08-31 20:40:31 +02:00
return $this -> getDatabaseService () -> ExecuteDbQuery ( $sql ) -> fetchAll ( \PDO :: FETCH_OBJ );
}
2020-01-26 20:01:30 +01:00
2020-08-31 20:40:31 +02:00
public function GetProductDetails ( int $productId )
{
if ( ! $this -> ProductExists ( $productId ))
{
throw new \Exception ( 'Product does not exist or is inactive' );
}
2019-12-19 12:48:36 -06:00
2020-08-31 20:40:31 +02:00
$stockCurrentRow = FindObjectinArrayByPropertyValue ( $this -> GetCurrentStock (), 'product_id' , $productId );
if ( $stockCurrentRow == null )
{
$stockCurrentRow = new \stdClass ();
$stockCurrentRow -> amount = 0 ;
$stockCurrentRow -> value = 0 ;
$stockCurrentRow -> amount_opened = 0 ;
$stockCurrentRow -> amount_aggregated = 0 ;
$stockCurrentRow -> amount_opened_aggregated = 0 ;
$stockCurrentRow -> is_aggregated_amount = 0 ;
}
2019-12-19 12:48:36 -06:00
2020-10-12 17:50:33 +02:00
$productLastPurchased = $this -> getDatabase () -> products_last_purchased () -> where ( 'product_id' , $productId ) -> fetch ();
$lastPurchasedDate = null ;
$lastPrice = null ;
$lastShoppingLocation = null ;
$avgPrice = null ;
$oldestPrice = null ;
if ( $productLastPurchased )
{
$lastPurchasedDate = $productLastPurchased -> purchased_date ;
$lastPrice = $productLastPurchased -> price ;
$lastShoppingLocation = $productLastPurchased -> shopping_location_id ;
2020-11-08 15:09:10 +01:00
$avgPriceRow = $this -> getDatabase () -> products_average_price () -> where ( 'product_id' , $productId ) -> fetch ();
if ( $avgPriceRow )
{
$avgPrice = $avgPriceRow -> price ;
}
$oldestPriceRow = $this -> getDatabase () -> products_oldest_stock_unit_price () -> where ( 'product_id' , $productId ) -> fetch ();
if ( $oldestPriceRow )
{
$oldestPrice = $avgPriceRow -> price ;
}
2020-10-12 17:50:33 +02:00
}
2020-08-31 20:40:31 +02:00
$product = $this -> getDatabase () -> products ( $productId );
$productBarcodes = $this -> getDatabase () -> product_barcodes () -> where ( 'product_id' , $productId ) -> fetchAll ();
$productLastUsed = $this -> getDatabase () -> stock_log () -> where ( 'product_id' , $productId ) -> where ( 'transaction_type' , self :: TRANSACTION_TYPE_CONSUME ) -> where ( 'undone' , 0 ) -> max ( 'used_date' );
2020-11-15 19:53:44 +01:00
$nextDueDate = $this -> getDatabase () -> stock () -> where ( 'product_id' , $productId ) -> min ( 'best_before_date' );
2020-08-31 20:40:31 +02:00
$quPurchase = $this -> getDatabase () -> quantity_units ( $product -> qu_id_purchase );
$quStock = $this -> getDatabase () -> quantity_units ( $product -> qu_id_stock );
$location = $this -> getDatabase () -> locations ( $product -> location_id );
$averageShelfLifeDays = intval ( $this -> getDatabase () -> stock_average_product_shelf_life () -> where ( 'id' , $productId ) -> fetch () -> average_shelf_life_days );
$defaultShoppingLocation = null ;
2019-12-19 12:48:36 -06:00
2021-06-28 19:43:08 +02:00
$consumeCount = $this -> getDatabase () -> stock_log () -> where ( 'product_id' , $productId ) -> where ( 'transaction_type' , self :: TRANSACTION_TYPE_CONSUME ) -> where ( 'undone = 0' ) -> sum ( 'amount' ) * - 1 ;
2020-08-31 20:40:31 +02:00
$consumeCountSpoiled = $this -> getDatabase () -> stock_log () -> where ( 'product_id' , $productId ) -> where ( 'transaction_type' , self :: TRANSACTION_TYPE_CONSUME ) -> where ( 'undone = 0 AND spoiled = 1' ) -> sum ( 'amount' ) * - 1 ;
2021-06-28 19:43:08 +02:00
if ( $consumeCount == 0 || $consumeCount == null )
2020-08-31 20:40:31 +02:00
{
$consumeCount = 1 ;
}
2021-06-28 19:43:08 +02:00
$spoilRate = ( $consumeCountSpoiled * 100.0 ) / $consumeCount ;
2019-12-19 12:48:36 -06:00
2020-08-31 20:40:31 +02:00
return [
'product' => $product ,
'product_barcodes' => $productBarcodes ,
2020-10-12 17:50:33 +02:00
'last_purchased' => $lastPurchasedDate ,
2020-08-31 20:40:31 +02:00
'last_used' => $productLastUsed ,
'stock_amount' => $stockCurrentRow -> amount ,
'stock_value' => $stockCurrentRow -> value ,
'stock_amount_opened' => $stockCurrentRow -> amount_opened ,
'stock_amount_aggregated' => $stockCurrentRow -> amount_aggregated ,
'stock_amount_opened_aggregated' => $stockCurrentRow -> amount_opened_aggregated ,
2020-11-13 17:30:57 +01:00
'default_quantity_unit_purchase' => $quPurchase ,
2020-08-31 20:40:31 +02:00
'quantity_unit_stock' => $quStock ,
'last_price' => $lastPrice ,
2020-10-12 17:50:33 +02:00
'avg_price' => $avgPrice ,
'oldest_price' => $oldestPrice ,
2020-08-31 20:40:31 +02:00
'last_shopping_location_id' => $lastShoppingLocation ,
'default_shopping_location_id' => $product -> shopping_location_id ,
2020-11-15 19:53:44 +01:00
'next_due_date' => $nextDueDate ,
2020-08-31 20:40:31 +02:00
'location' => $location ,
'average_shelf_life_days' => $averageShelfLifeDays ,
'spoil_rate_percent' => $spoilRate ,
2021-07-11 10:21:36 +02:00
'is_aggregated_amount' => $stockCurrentRow -> is_aggregated_amount ,
'has_childs' => $this -> getDatabase () -> products () -> where ( 'parent_product_id = :1' , $product -> id ) -> count () !== 0 ,
2020-08-31 20:40:31 +02:00
];
}
2019-12-19 12:48:36 -06:00
2020-08-31 20:40:31 +02:00
public function GetProductIdFromBarcode ( string $barcode )
{
2021-06-12 17:21:12 +02:00
// first, try to parse this as a product grocycode
if ( Grocycode :: Validate ( $barcode ))
{
$gc = new Grocycode ( $barcode );
return intval ( $gc -> GetId ());
}
2020-08-31 20:40:31 +02:00
$potentialProduct = $this -> getDatabase () -> product_barcodes () -> where ( 'barcode = :1' , $barcode ) -> fetch ();
if ( $potentialProduct === null )
{
throw new \Exception ( " No product with barcode $barcode found " );
2019-12-19 12:48:36 -06:00
}
2020-10-18 14:09:54 +02:00
return intval ( $potentialProduct -> product_id );
2019-12-19 12:48:36 -06:00
}
2020-08-31 20:40:31 +02:00
public function GetProductPriceHistory ( int $productId )
2019-12-19 12:48:36 -06:00
{
2020-08-31 20:40:31 +02:00
if ( ! $this -> ProductExists ( $productId ))
{
throw new \Exception ( 'Product does not exist or is inactive' );
}
2019-12-19 12:48:36 -06:00
2020-08-31 20:40:31 +02:00
$returnData = [];
$shoppingLocations = $this -> getDatabase () -> shopping_locations ();
2019-12-19 12:48:36 -06:00
2021-07-16 17:32:08 +02:00
$rows = $this -> getDatabase () -> product_price_history () -> where ( 'product_id = :1' , $productId ) -> orderBy ( 'purchased_date' , 'DESC' );
2020-08-31 20:40:31 +02:00
foreach ( $rows as $row )
2019-12-19 12:48:36 -06:00
{
2020-08-31 20:40:31 +02:00
$returnData [] = [
'date' => $row -> purchased_date ,
'price' => $row -> price ,
'shopping_location' => FindObjectInArrayByPropertyValue ( $shoppingLocations , 'id' , $row -> shopping_location_id )
];
2019-12-19 12:48:36 -06:00
}
2020-08-31 20:40:31 +02:00
return $returnData ;
}
2019-12-19 12:48:36 -06:00
2020-09-01 19:59:40 +02:00
public function GetProductStockEntries ( $productId , $excludeOpened = false , $allowSubproductSubstitution = false , $ordered = true )
2020-08-31 20:40:31 +02:00
{
2020-11-19 18:28:16 +01:00
$sqlWhereProductId = 'product_id = ' . $productId ;
2020-08-31 20:40:31 +02:00
if ( $allowSubproductSubstitution )
2020-01-22 21:17:04 +01:00
{
2020-12-07 19:48:33 +01:00
$sqlWhereProductId = '(product_id IN (SELECT sub_product_id FROM products_resolved WHERE parent_product_id = ' . $productId . ') OR product_id = ' . $productId . ')' ;
2020-01-22 21:17:04 +01:00
}
2020-08-31 20:40:31 +02:00
$sqlWhereAndOpen = 'AND open IN (0, 1)' ;
if ( $excludeOpened )
2020-01-22 21:17:04 +01:00
{
2020-08-31 20:40:31 +02:00
$sqlWhereAndOpen = 'AND open = 0' ;
2020-01-22 14:08:49 -06:00
}
2020-11-19 18:37:16 +01:00
2020-11-19 18:28:16 +01:00
$result = $this -> getDatabase () -> stock () -> where ( $sqlWhereProductId . ' ' . $sqlWhereAndOpen );
2020-11-19 18:37:16 +01:00
// In order of next use:
2020-12-19 10:28:35 +01:00
// Opened first, then first due first, then first in first out
2020-09-01 19:59:40 +02:00
if ( $ordered )
2020-09-01 21:29:47 +02:00
{
2020-12-19 10:28:35 +01:00
return $result -> orderBy ( 'open' , 'DESC' ) -> orderBy ( 'best_before_date' , 'ASC' ) -> orderBy ( 'purchased_date' , 'ASC' );
2020-09-01 21:29:47 +02:00
}
2020-11-19 18:37:16 +01:00
2020-09-01 19:59:40 +02:00
return $result ;
2020-08-31 20:40:31 +02:00
}
2019-12-19 12:48:36 -06:00
2020-08-31 20:40:31 +02:00
public function GetProductStockEntriesForLocation ( $productId , $locationId , $excludeOpened = false , $allowSubproductSubstitution = false )
{
2020-12-19 10:28:35 +01:00
$stockEntries = $this -> GetProductStockEntries ( $productId , $excludeOpened , $allowSubproductSubstitution , true );
2020-08-31 20:40:31 +02:00
return FindAllObjectsInArrayByPropertyValue ( $stockEntries , 'location_id' , $locationId );
}
2019-12-19 12:48:36 -06:00
2020-11-19 18:37:16 +01:00
public function GetProductStockLocations ( $productId , $allowSubproductSubstitution = false )
2020-08-31 20:40:31 +02:00
{
2020-11-19 18:37:16 +01:00
$sqlWhereProductId = 'product_id = ' . $productId ;
if ( $allowSubproductSubstitution )
{
2020-12-07 19:48:33 +01:00
$sqlWhereProductId = '(product_id IN (SELECT sub_product_id FROM products_resolved WHERE parent_product_id = ' . $productId . ') OR product_id = ' . $productId . ')' ;
2020-11-19 18:37:16 +01:00
}
return $this -> getDatabase () -> stock_current_locations () -> where ( $sqlWhereProductId );
2020-08-31 20:40:31 +02:00
}
public function GetStockEntry ( $entryId )
{
return $this -> getDatabase () -> stock () -> where ( 'id' , $entryId ) -> fetch ();
2019-12-19 12:48:36 -06:00
}
2020-01-21 20:04:33 +01:00
2021-07-05 17:48:34 +02:00
public function InventoryProduct ( int $productId , float $newAmount , $bestBeforeDate , $locationId = null , $price = null , $shoppingLocationId = null , $purchasedDate = null )
2017-04-20 17:10:21 +02:00
{
2018-04-22 14:25:08 +02:00
if ( ! $this -> ProductExists ( $productId ))
{
2020-08-17 14:47:33 -05:00
throw new \Exception ( 'Product does not exist or is inactive' );
2018-04-22 14:25:08 +02:00
}
2017-04-20 17:10:21 +02:00
2021-06-12 17:21:12 +02:00
$productDetails = ( object ) $this -> GetProductDetails ( $productId );
2019-03-05 17:51:50 +01:00
2019-05-03 22:11:20 +02:00
if ( $price === null )
{
$price = $productDetails -> last_price ;
}
2020-03-25 19:34:56 +01:00
if ( $shoppingLocationId === null )
{
$shoppingLocationId = $productDetails -> last_shopping_location_id ;
}
2021-07-05 17:48:34 +02:00
if ( $purchasedDate == null )
{
$purchasedDate = date ( 'Y-m-d' );
}
2020-09-01 21:29:47 +02:00
// Tare weight handling
// The given amount is the new total amount including the container weight (gross)
2019-03-05 17:51:50 +01:00
// So assume that the amount in stock is the amount also including the container weight
$containerWeight = 0 ;
2020-08-31 20:40:31 +02:00
2019-03-05 17:51:50 +01:00
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
}
2020-01-19 09:14:07 +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' );
}
2020-09-01 21:29:47 +02:00
elseif ( $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 );
2020-08-31 20:40:31 +02:00
2019-03-05 17:51:50 +01:00
if ( $productDetails -> product -> enable_tare_weight_handling == 1 )
{
$bookingAmount = $newAmount ;
}
2020-01-19 09:14:07 +01:00
2020-11-10 18:11:33 +01:00
return $this -> AddProduct ( $productId , $bookingAmount , $bestBeforeDate , self :: TRANSACTION_TYPE_INVENTORY_CORRECTION , $purchasedDate , $price , $locationId , $shoppingLocationId );
2017-04-20 17:10:21 +02:00
}
2020-09-01 21:29:47 +02:00
elseif ( $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 ;
2020-08-31 20:40:31 +02:00
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 -> 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
2020-12-07 19:48:33 +01:00
public function OpenProduct ( int $productId , float $amount , $specificStockEntryId = 'default' , & $transactionId = null , $allowSubproductSubstitution = false )
2018-11-17 19:39:37 +01:00
{
if ( ! $this -> ProductExists ( $productId ))
{
2020-08-17 14:47:33 -05:00
throw new \Exception ( 'Product does not exist or is inactive' );
2018-11-17 19:39:37 +01:00
}
2021-06-12 17:21:12 +02:00
$productDetails = ( object ) $this -> GetProductDetails ( $productId );
2020-12-07 19:48:33 +01:00
$productStockAmountUnopened = floatval ( $productDetails -> stock_amount_aggregated ) - floatval ( $productDetails -> stock_amount_opened_aggregated );
$potentialStockEntries = $this -> GetProductStockEntries ( $productId , true , $allowSubproductSubstitution );
2020-03-01 23:47:47 +07:00
$product = $this -> getDatabase () -> products ( $productId );
2018-11-17 19:39:37 +01:00
2020-11-14 22:51:06 +01:00
if ( $product -> enable_tare_weight_handling == 1 )
{
throw new \Exception ( 'Opening tare weight handling enabled products is not supported' );
}
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 );
}
2020-04-13 17:04:59 +02:00
if ( $transactionId === null )
{
$transactionId = uniqid ();
}
2018-11-17 19:39:37 +01:00
foreach ( $potentialStockEntries as $stockEntry )
{
if ( $amount == 0 )
{
break ;
}
$newBestBeforeDate = $stockEntry -> best_before_date ;
if ( $product -> default_best_before_days_after_open > 0 )
{
2020-08-31 20:40:31 +02:00
$newBestBeforeDate = date ( 'Y-m-d' , strtotime ( '+' . $product -> default_best_before_days_after_open . ' days' ));
2021-07-12 17:56:09 +02:00
// The new due date should be never > the original due date
if ( strtotime ( $newBestBeforeDate ) > strtotime ( $stockEntry -> best_before_date ))
{
$newBestBeforeDate = $stockEntry -> best_before_date ;
}
2018-11-17 19:39:37 +01:00
}
2020-12-07 19:48:33 +01:00
if ( $allowSubproductSubstitution && $stockEntry -> product_id != $productId )
{
// A sub product will be used -> use QU conversions
$subProduct = $this -> getDatabase () -> products ( $stockEntry -> product_id );
$conversion = $this -> getDatabase () -> quantity_unit_conversions_resolved () -> where ( 'product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3' , $stockEntry -> product_id , $product -> qu_id_stock , $subProduct -> qu_id_stock ) -> fetch ();
if ( $conversion != null )
{
$amount = $amount * floatval ( $conversion -> factor );
}
}
2020-09-01 21:29:47 +02:00
if ( $amount >= $stockEntry -> amount )
2020-12-07 19:48:33 +01:00
{
// Mark the whole stock entry as opened
2020-08-31 20:40:31 +02:00
$logRow = $this -> getDatabase () -> stock_log () -> createRow ([
2018-11-17 19:39:37 +01:00
'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 ,
2020-08-29 05:26:36 -05:00
'location_id' => $stockEntry -> location_id ,
'shopping_location_id' => $stockEntry -> shopping_location_id ,
2018-11-17 19:39:37 +01:00
'transaction_type' => self :: TRANSACTION_TYPE_PRODUCT_OPENED ,
'price' => $stockEntry -> price ,
2020-04-13 17:04:59 +02:00
'opened_date' => date ( 'Y-m-d' ),
2020-09-06 13:18:51 +02:00
'transaction_id' => $transactionId ,
'user_id' => GROCY_USER_ID
2020-08-31 20:40:31 +02:00
]);
2018-11-17 19:39:37 +01:00
$logRow -> save ();
2020-08-31 20:40:31 +02:00
$stockEntry -> update ([
2018-11-17 19:39:37 +01:00
'open' => 1 ,
'opened_date' => date ( 'Y-m-d' ),
'best_before_date' => $newBestBeforeDate
2020-08-31 20:40:31 +02:00
]);
2018-11-18 12:34:05 +01:00
$amount -= $stockEntry -> amount ;
2018-11-17 19:39:37 +01:00
}
2020-09-01 21:29:47 +02:00
else
2020-12-07 19:48:33 +01:00
{
// Stock entry amount is > than needed amount -> split the stock entry
2018-11-18 12:34:05 +01:00
$restStockAmount = $stockEntry -> amount - $amount ;
2020-08-31 20:40:31 +02:00
$newStockRow = $this -> getDatabase () -> stock () -> createRow ([
2018-11-18 12:34:05 +01:00
'product_id' => $stockEntry -> product_id ,
'amount' => $restStockAmount ,
'best_before_date' => $stockEntry -> best_before_date ,
'purchased_date' => $stockEntry -> purchased_date ,
2020-08-29 05:26:36 -05:00
'location_id' => $stockEntry -> location_id ,
'shopping_location_id' => $stockEntry -> shopping_location_id ,
2018-11-18 12:34:05 +01:00
'stock_id' => $stockEntry -> stock_id ,
'price' => $stockEntry -> price
2020-08-31 20:40:31 +02:00
]);
2018-11-18 12:34:05 +01:00
$newStockRow -> save ();
2020-08-31 20:40:31 +02:00
$logRow = $this -> getDatabase () -> stock_log () -> createRow ([
2018-11-17 19:39:37 +01:00
'product_id' => $stockEntry -> product_id ,
'amount' => $amount ,
'best_before_date' => $stockEntry -> best_before_date ,
'purchased_date' => $stockEntry -> purchased_date ,
'stock_id' => $stockEntry -> stock_id ,
2020-08-29 05:26:36 -05:00
'location_id' => $stockEntry -> location_id ,
'shopping_location_id' => $stockEntry -> shopping_location_id ,
2018-11-17 19:39:37 +01:00
'transaction_type' => self :: TRANSACTION_TYPE_PRODUCT_OPENED ,
'price' => $stockEntry -> price ,
2020-04-13 17:04:59 +02:00
'opened_date' => date ( 'Y-m-d' ),
2020-09-06 13:18:51 +02:00
'transaction_id' => $transactionId ,
'user_id' => GROCY_USER_ID
2020-08-31 20:40:31 +02:00
]);
2018-11-17 19:39:37 +01:00
$logRow -> save ();
2020-08-31 20:40:31 +02:00
$stockEntry -> update ([
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
2020-08-31 20:40:31 +02:00
]);
2018-11-18 12:34:05 +01:00
$amount = 0 ;
2018-11-17 19:39:37 +01:00
}
}
2020-12-20 14:43:07 +01:00
return $transactionId ;
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
2020-03-01 23:47:47 +07:00
$productRow = $this -> getDatabase () -> shopping_list () -> where ( 'product_id = :1' , $productId ) -> fetch ();
2019-08-10 08:20:52 +02:00
2020-09-01 21:29:47 +02:00
//If no entry was found with for this product, we return gracefully
2019-08-04 20:31:47 +02:00
if ( $productRow != null && ! empty ( $productRow ))
{
2020-12-23 19:56:37 +01:00
$decimals = intval ( $this -> getUsersService () -> GetUserSetting ( GROCY_USER_ID , 'stock_decimal_places_amounts' ));
2019-08-04 20:31:47 +02:00
$newAmount = $productRow -> amount - $amount ;
2021-07-16 17:32:08 +02:00
2020-12-23 19:56:37 +01:00
if ( $newAmount < floatval ( '0.' . str_repeat ( '0' , $decimals - ( $decimals <= 0 ? 0 : 1 )) . '1' ))
2019-08-04 20:31:47 +02:00
{
$productRow -> delete ();
2019-08-10 08:20:52 +02:00
}
else
{
2020-08-31 20:40:31 +02:00
$productRow -> update ([ 'amount' => $newAmount ]);
2019-08-04 20:31:47 +02:00
}
}
}
2021-06-18 20:45:42 +02:00
/**
* Returns the shoppinglist as an array with lines for a printer
* @ param int $listId ID of shopping list
* @ return string [] Returns an array in the format " [amount] [name of product] "
* @ throws \Exception
*/
public function GetShoppinglistInPrintableStrings ( $listId = 1 ) : array
{
if ( ! $this -> ShoppingListExists ( $listId ))
{
throw new \Exception ( 'Shopping list does not exist' );
}
2021-06-27 20:55:38 +02:00
$result_product = [];
$result_quantity = [];
2021-06-18 20:45:42 +02:00
$rowsShoppingListProducts = $this -> getDatabase () -> uihelper_shopping_list () -> where ( 'shopping_list_id = :1' , $listId ) -> fetchAll ();
foreach ( $rowsShoppingListProducts as $row )
{
2021-06-27 20:55:38 +02:00
$isValidProduct = ( $row -> product_id != null && $row -> product_id != '' );
2021-06-18 20:45:42 +02:00
if ( $isValidProduct )
{
2021-06-27 20:55:38 +02:00
$product = $this -> getDatabase () -> products () -> where ( 'id = :1' , $row -> product_id ) -> fetch ();
2021-06-18 20:45:42 +02:00
$conversion = $this -> getDatabase () -> quantity_unit_conversions_resolved () -> where ( 'product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3' , $product -> id , $product -> qu_id_stock , $row -> qu_id ) -> fetch ();
2021-07-16 17:32:08 +02:00
2021-06-27 20:55:38 +02:00
$factor = 1.0 ;
2021-06-18 20:45:42 +02:00
if ( $conversion != null )
{
$factor = floatval ( $conversion -> factor );
}
2021-07-16 17:32:08 +02:00
2021-06-18 20:45:42 +02:00
$amount = round ( $row -> amount * $factor );
2021-06-27 20:55:38 +02:00
$note = '' ;
2021-07-16 17:32:08 +02:00
2021-06-18 20:45:42 +02:00
if ( GROCY_TPRINTER_PRINT_NOTES )
{
2021-06-27 20:55:38 +02:00
if ( $row -> note != '' )
{
2021-06-18 20:45:42 +02:00
$note = ' (' . $row -> note . ')' ;
}
}
}
2021-07-16 17:32:08 +02:00
2021-06-18 20:45:42 +02:00
if ( GROCY_TPRINTER_PRINT_QUANTITY_NAME && $isValidProduct )
{
$quantityname = $row -> qu_name ;
if ( $amount > 1 )
{
$quantityname = $row -> qu_name_plural ;
}
2021-07-16 17:32:08 +02:00
2021-06-18 20:45:42 +02:00
array_push ( $result_quantity , $amount . ' ' . $quantityname );
array_push ( $result_product , $row -> product_name . $note );
}
else
{
if ( $isValidProduct )
{
array_push ( $result_quantity , $amount );
array_push ( $result_product , $row -> product_name . $note );
}
else
{
array_push ( $result_quantity , round ( $row -> amount ));
array_push ( $result_product , $row -> note );
}
}
}
2021-07-16 17:32:08 +02:00
2021-06-18 20:45:42 +02:00
//Add padding to look nicer
$maxlength = 1 ;
foreach ( $result_quantity as $quantity )
{
if ( strlen ( $quantity ) > $maxlength )
{
$maxlength = strlen ( $quantity );
}
}
2021-07-16 17:32:08 +02:00
2021-06-27 20:55:38 +02:00
$result = [];
2021-06-18 20:45:42 +02:00
$length = count ( $result_quantity );
for ( $i = 0 ; $i < $length ; $i ++ )
{
$quantity = str_pad ( $result_quantity [ $i ], $maxlength );
array_push ( $result , $quantity . ' ' . $result_product [ $i ]);
}
2021-07-16 17:32:08 +02:00
2021-06-18 20:45:42 +02:00
return $result ;
}
2020-08-31 20:40:31 +02:00
public function TransferProduct ( int $productId , float $amount , int $locationIdFrom , int $locationIdTo , $specificStockEntryId = 'default' , & $transactionId = null )
2019-08-30 09:21:11 +02:00
{
2019-08-31 14:08:15 +02:00
if ( ! $this -> ProductExists ( $productId ))
{
2020-08-17 14:47:33 -05:00
throw new \Exception ( 'Product does not exist or is inactive' );
2019-08-31 14:08:15 +02:00
}
2020-08-31 20:40:31 +02:00
if ( ! $this -> LocationExists ( $locationIdFrom ))
2019-08-31 14:08:15 +02:00
{
2020-08-31 20:40:31 +02:00
throw new \Exception ( 'Source location does not exist' );
2019-08-31 14:08:15 +02:00
}
2020-08-31 20:40:31 +02:00
if ( ! $this -> LocationExists ( $locationIdTo ))
2019-08-31 14:08:15 +02:00
{
2020-08-31 20:40:31 +02:00
throw new \Exception ( 'Destination location does not exist' );
2019-08-31 14:08:15 +02:00
}
2018-04-22 19:47:46 +02:00
2020-09-01 21:29:47 +02:00
// Tare weight handling
// The given amount is the new total amount including the container weight (gross)
2020-08-31 20:40:31 +02:00
// The amount to be posted needs to be the absolute value of the given amount - stock amount - tare weight
$productDetails = ( object ) $this -> GetProductDetails ( $productId );
2019-04-22 08:21:57 +02:00
2020-08-31 20:40:31 +02:00
if ( $productDetails -> product -> enable_tare_weight_handling == 1 )
2018-04-22 19:47:46 +02:00
{
2021-03-31 21:12:51 +01:00
// Hard fail for now, as we not yet support transferring tare weight enabled products
throw new \Exception ( 'Transferring tare weight enabled products is not yet possible' );
2020-08-31 20:40:31 +02:00
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 ));
2018-04-22 19:47:46 +02:00
}
2020-08-31 20:40:31 +02:00
$productStockAmountAtFromLocation = $this -> getDatabase () -> stock () -> where ( 'product_id = :1 AND location_id = :2' , $productId , $locationIdFrom ) -> sum ( 'amount' );
$potentialStockEntriesAtFromLocation = $this -> GetProductStockEntriesForLocation ( $productId , $locationIdFrom );
if ( $amount > $productStockAmountAtFromLocation )
2018-04-22 19:47:46 +02:00
{
2021-03-31 21:12:51 +01:00
throw new \Exception ( 'Amount to be transferred cannot be > current stock amount at the source location' );
2018-04-22 19:47:46 +02:00
}
2020-08-31 20:40:31 +02:00
if ( $specificStockEntryId !== 'default' )
2018-04-22 19:47:46 +02:00
{
2020-08-31 20:40:31 +02:00
$potentialStockEntriesAtFromLocation = FindAllObjectsInArrayByPropertyValue ( $potentialStockEntriesAtFromLocation , 'stock_id' , $specificStockEntryId );
2018-04-22 19:47:46 +02:00
}
2020-08-31 20:40:31 +02:00
if ( $transactionId === null )
{
$transactionId = uniqid ();
}
2018-04-22 19:47:46 +02:00
2020-08-31 20:40:31 +02:00
foreach ( $potentialStockEntriesAtFromLocation as $stockEntry )
2018-04-22 19:47:46 +02:00
{
2020-08-31 20:40:31 +02:00
if ( $amount == 0 )
2018-04-22 19:47:46 +02:00
{
2020-08-31 20:40:31 +02:00
break ;
}
2018-04-22 19:47:46 +02:00
2020-08-31 20:40:31 +02:00
$newBestBeforeDate = $stockEntry -> best_before_date ;
if ( GROCY_FEATURE_FLAG_STOCK_PRODUCT_FREEZING )
{
$locationFrom = $this -> getDatabase () -> locations () -> where ( 'id' , $locationIdFrom ) -> fetch ();
$locationTo = $this -> getDatabase () -> locations () -> where ( 'id' , $locationIdTo ) -> fetch ();
2020-09-01 21:29:47 +02:00
// Product was moved from a non-freezer to freezer location -> freeze
2020-12-19 17:32:47 +01:00
if ( intval ( $locationFrom -> is_freezer ) === 0 && intval ( $locationTo -> is_freezer ) === 1 && $productDetails -> product -> default_best_before_days_after_freezing >= - 1 )
2020-08-31 20:40:31 +02:00
{
2020-12-19 17:32:47 +01:00
if ( $productDetails -> product -> default_best_before_days_after_freezing == - 1 )
{
$newBestBeforeDate = date ( '2999-12-31' );
}
else
{
$newBestBeforeDate = date ( 'Y-m-d' , strtotime ( '+' . $productDetails -> product -> default_best_before_days_after_freezing . ' days' ));
}
2020-08-31 20:40:31 +02:00
}
2020-09-01 21:29:47 +02:00
// Product was moved from a freezer to non-freezer location -> thaw
2020-08-31 20:40:31 +02:00
if ( intval ( $locationFrom -> is_freezer ) === 1 && intval ( $locationTo -> is_freezer ) === 0 && $productDetails -> product -> default_best_before_days_after_thawing > 0 )
{
$newBestBeforeDate = date ( 'Y-m-d' , strtotime ( '+' . $productDetails -> product -> default_best_before_days_after_thawing . ' days' ));
}
}
$correlationId = uniqid ();
2020-09-01 21:29:47 +02:00
if ( $amount >= $stockEntry -> amount )
2020-11-10 18:11:33 +01:00
{
// Take the whole stock entry
2020-08-31 20:40:31 +02:00
$logRowForLocationFrom = $this -> getDatabase () -> stock_log () -> createRow ([
'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 ,
2021-06-27 20:55:38 +02:00
'shopping_location_id' => $stockEntry -> shopping_location_id ,
2020-08-31 20:40:31 +02:00
'correlation_id' => $correlationId ,
2020-09-06 13:18:51 +02:00
'transaction_Id' => $transactionId ,
'user_id' => GROCY_USER_ID
2020-08-31 20:40:31 +02:00
]);
$logRowForLocationFrom -> save ();
$logRowForLocationTo = $this -> getDatabase () -> stock_log () -> createRow ([
'product_id' => $stockEntry -> product_id ,
'amount' => $stockEntry -> amount ,
'best_before_date' => $newBestBeforeDate ,
'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 ,
2021-06-27 20:55:38 +02:00
'shopping_location_id' => $stockEntry -> shopping_location_id ,
2020-08-31 20:40:31 +02:00
'correlation_id' => $correlationId ,
2020-09-06 13:18:51 +02:00
'transaction_Id' => $transactionId ,
'user_id' => GROCY_USER_ID
2020-08-31 20:40:31 +02:00
]);
$logRowForLocationTo -> save ();
$stockEntry -> update ([
'location_id' => $locationIdTo ,
'best_before_date' => $newBestBeforeDate
]);
$amount -= $stockEntry -> amount ;
}
2020-09-01 21:29:47 +02:00
else
2021-07-16 17:32:08 +02:00
{
// Stock entry amount is > than needed amount -> split the stock entry resp. update the amount
2020-08-31 20:40:31 +02:00
$restStockAmount = $stockEntry -> amount - $amount ;
$logRowForLocationFrom = $this -> getDatabase () -> stock_log () -> createRow ([
'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 ,
2021-06-27 20:55:38 +02:00
'shopping_location_id' => $stockEntry -> shopping_location_id ,
2020-08-31 20:40:31 +02:00
'correlation_id' => $correlationId ,
2020-09-06 13:18:51 +02:00
'transaction_Id' => $transactionId ,
'user_id' => GROCY_USER_ID
2020-08-31 20:40:31 +02:00
]);
$logRowForLocationFrom -> save ();
$logRowForLocationTo = $this -> getDatabase () -> stock_log () -> createRow ([
'product_id' => $stockEntry -> product_id ,
'amount' => $amount ,
'best_before_date' => $newBestBeforeDate ,
'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 ,
2021-06-27 20:55:38 +02:00
'shopping_location_id' => $stockEntry -> shopping_location_id ,
2020-08-31 20:40:31 +02:00
'correlation_id' => $correlationId ,
2020-09-06 13:18:51 +02:00
'transaction_Id' => $transactionId ,
'user_id' => GROCY_USER_ID
2020-08-31 20:40:31 +02:00
]);
$logRowForLocationTo -> save ();
// This is the existing stock entry -> remains at the source location with the rest amount
$stockEntry -> update ([
'amount' => $restStockAmount
]);
2021-03-31 21:12:51 +01:00
// The transferred amount gets into a new stock entry
2020-08-31 20:40:31 +02:00
$stockEntryNew = $this -> getDatabase () -> stock () -> createRow ([
'product_id' => $stockEntry -> product_id ,
'amount' => $amount ,
'best_before_date' => $newBestBeforeDate ,
'purchased_date' => $stockEntry -> purchased_date ,
'stock_id' => $stockEntry -> stock_id ,
'price' => $stockEntry -> price ,
'location_id' => $locationIdTo ,
2021-06-27 20:55:38 +02:00
'shopping_location_id' => $stockEntry -> shopping_location_id ,
2020-08-31 20:40:31 +02:00
'open' => $stockEntry -> open ,
'opened_date' => $stockEntry -> opened_date
]);
$stockEntryNew -> save ();
$amount = 0 ;
2018-04-22 19:47:46 +02:00
}
}
2020-12-20 14:43:07 +01:00
return $transactionId ;
2018-04-22 19:47:46 +02:00
}
2018-10-26 22:28:58 +02:00
2019-12-19 12:48:36 -06:00
public function UndoBooking ( $bookingId , $skipCorrelatedBookings = false )
2018-10-26 22:28:58 +02:00
{
2020-03-01 23:47:47 +07:00
$logRow = $this -> getDatabase () -> 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' );
}
2020-09-01 21:29:47 +02:00
// Undo all correlated bookings first, in order from newest first to the oldest
2019-12-19 12:48:36 -06:00
if ( ! $skipCorrelatedBookings && ! empty ( $logRow -> correlation_id ))
{
2020-03-01 23:47:47 +07:00
$correlatedBookings = $this -> getDatabase () -> stock_log () -> where ( 'undone = 0 AND correlation_id = :1' , $logRow -> correlation_id ) -> orderBy ( 'id' , 'DESC' ) -> fetchAll ();
2019-12-19 12:48:36 -06:00
foreach ( $correlatedBookings as $correlatedBooking )
{
$this -> UndoBooking ( $correlatedBooking -> id , true );
}
2020-08-31 20:40:31 +02:00
2019-12-19 12:48:36 -06:00
return ;
}
2021-07-11 21:06:05 +02:00
$hasSubsequentBookings = $this -> getDatabase () -> 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 ;
2018-10-27 10:19:06 +02:00
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
2020-03-01 23:47:47 +07:00
$stockRows = $this -> getDatabase () -> stock () -> where ( 'stock_id' , $logRow -> stock_id );
2018-10-26 22:28:58 +02:00
$stockRows -> delete ();
// Update log entry
2020-08-31 20:40:31 +02:00
$logRow -> update ([
2018-10-26 22:28:58 +02:00
'undone' => 1 ,
'undone_timestamp' => date ( 'Y-m-d H:i:s' )
2020-08-31 20:40:31 +02:00
]);
2018-10-26 22:28:58 +02:00
}
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
2020-08-31 20:40:31 +02:00
$stockRow = $this -> getDatabase () -> stock () -> createRow ([
2018-10-26 22:28:58 +02:00
'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 ,
2020-12-16 17:44:51 +01:00
'opened_date' => $logRow -> opened_date ,
'open' => $logRow -> opened_date !== null
2020-08-31 20:40:31 +02:00
]);
2018-10-26 22:28:58 +02:00
$stockRow -> save ();
// Update log entry
2020-08-31 20:40:31 +02:00
$logRow -> update ([
2018-10-26 22:28:58 +02:00
'undone' => 1 ,
'undone_timestamp' => date ( 'Y-m-d H:i:s' )
2020-08-31 20:40:31 +02:00
]);
2018-10-26 22:28:58 +02:00
}
2019-12-19 12:48:36 -06:00
elseif ( $logRow -> transaction_type === self :: TRANSACTION_TYPE_TRANSFER_TO )
{
2020-03-01 23:47:47 +07:00
$stockRow = $this -> getDatabase () -> stock () -> where ( 'stock_id = :1 AND location_id = :2' , $logRow -> stock_id , $logRow -> location_id ) -> fetch ();
2019-12-19 12:48:36 -06:00
if ( $stockRow === null )
{
throw new \Exception ( 'Booking does not exist or was already undone' );
}
2020-08-31 20:40:31 +02:00
2019-12-19 12:48:36 -06:00
$newAmount = $stockRow -> amount - $logRow -> amount ;
if ( $newAmount == 0 )
{
$stockRow -> delete ();
2020-08-31 20:40:31 +02:00
}
else
{
// Remove corresponding amount back to stock
$stockRow -> update ([
2019-12-19 12:48:36 -06:00
'amount' => $newAmount
2020-08-31 20:40:31 +02:00
]);
2019-12-19 12:48:36 -06:00
}
// Update log entry
2020-08-31 20:40:31 +02:00
$logRow -> update ([
2019-12-19 12:48:36 -06:00
'undone' => 1 ,
'undone_timestamp' => date ( 'Y-m-d H:i:s' )
2020-08-31 20:40:31 +02:00
]);
2019-12-19 12:48:36 -06:00
}
elseif ( $logRow -> transaction_type === self :: TRANSACTION_TYPE_TRANSFER_FROM )
{
2020-12-16 17:44:51 +01:00
// Add corresponding amount back to stock
2020-03-01 23:47:47 +07:00
$stockRow = $this -> getDatabase () -> stock () -> where ( 'stock_id = :1 AND location_id = :2' , $logRow -> stock_id , $logRow -> location_id ) -> fetch ();
2019-12-19 12:48:36 -06:00
if ( $stockRow === null )
{
2020-08-31 20:40:31 +02:00
$stockRow = $this -> getDatabase () -> stock () -> createRow ([
2019-12-19 12:48:36 -06:00
'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
2020-08-31 20:40:31 +02:00
]);
2019-12-19 12:48:36 -06:00
$stockRow -> save ();
2020-08-31 20:40:31 +02:00
}
else
{
$stockRow -> update ([
'amount' => $stockRow -> amount - $logRow -> amount
]);
2019-12-19 12:48:36 -06:00
}
// Update log entry
2020-08-31 20:40:31 +02:00
$logRow -> update ([
2019-12-19 12:48:36 -06:00
'undone' => 1 ,
'undone_timestamp' => date ( 'Y-m-d H:i:s' )
2020-08-31 20:40:31 +02:00
]);
2019-12-19 12:48:36 -06:00
}
2018-11-17 19:39:37 +01:00
elseif ( $logRow -> transaction_type === self :: TRANSACTION_TYPE_PRODUCT_OPENED )
{
2020-12-16 17:44:51 +01:00
// Remove opened flag from corresponding stock entry
2020-03-01 23:47:47 +07:00
$stockRows = $this -> getDatabase () -> stock () -> where ( 'stock_id = :1 AND amount = :2 AND purchased_date = :3' , $logRow -> stock_id , $logRow -> amount , $logRow -> purchased_date ) -> limit ( 1 );
2020-08-31 20:40:31 +02:00
$stockRows -> update ([
2018-11-17 19:39:37 +01:00
'open' => 0 ,
'opened_date' => null
2020-08-31 20:40:31 +02:00
]);
2018-11-17 19:39:37 +01:00
// Update log entry
2020-08-31 20:40:31 +02:00
$logRow -> update ([
2018-11-17 19:39:37 +01:00
'undone' => 1 ,
'undone_timestamp' => date ( 'Y-m-d H:i:s' )
2020-08-31 20:40:31 +02:00
]);
2018-11-17 19:39:37 +01:00
}
2019-12-19 12:48:36 -06:00
elseif ( $logRow -> transaction_type === self :: TRANSACTION_TYPE_STOCK_EDIT_NEW )
{
// Update log entry, no action needed
2020-08-31 20:40:31 +02:00
$logRow -> update ([
2019-12-19 12:48:36 -06:00
'undone' => 1 ,
'undone_timestamp' => date ( 'Y-m-d H:i:s' )
2020-08-31 20:40:31 +02:00
]);
2019-12-19 12:48:36 -06:00
}
elseif ( $logRow -> transaction_type === self :: TRANSACTION_TYPE_STOCK_EDIT_OLD )
{
// Make sure there is a stock row still
2020-03-01 23:47:47 +07:00
$stockRow = $this -> getDatabase () -> stock () -> where ( 'id = :1' , $logRow -> stock_row_id ) -> fetch ();
2020-08-31 20:40:31 +02:00
2019-12-19 12:48:36 -06:00
if ( $stockRow == null )
{
throw new \Exception ( 'Booking does not exist or was already undone' );
}
2020-01-22 22:36:01 +01:00
$openedDate = $logRow -> opened_date ;
2020-01-22 14:08:49 -06:00
$open = true ;
2020-01-22 22:36:01 +01:00
if ( $openedDate == null )
{
2020-01-22 14:08:49 -06:00
$open = false ;
}
2020-08-31 20:40:31 +02:00
$stockRow -> update ([
2019-12-19 12:48:36 -06:00
'amount' => $logRow -> amount ,
'best_before_date' => $logRow -> best_before_date ,
'purchased_date' => $logRow -> purchased_date ,
'price' => $logRow -> price ,
2020-01-22 14:08:49 -06:00
'location_id' => $logRow -> location_id ,
'open' => $open ,
2020-01-23 02:34:17 -06:00
'opened_date' => $openedDate
2020-08-31 20:40:31 +02:00
]);
2019-12-19 12:48:36 -06:00
// Update log entry
2020-08-31 20:40:31 +02:00
$logRow -> update ([
2019-12-19 12:48:36 -06:00
'undone' => 1 ,
'undone_timestamp' => date ( 'Y-m-d H:i:s' )
2020-08-31 20:40:31 +02:00
]);
2019-12-19 12:48:36 -06:00
}
2018-10-26 22:28:58 +02:00
else
{
throw new \Exception ( 'This booking cannot be undone' );
}
}
2019-12-19 12:48:36 -06:00
public function UndoTransaction ( $transactionId )
{
2020-03-01 23:47:47 +07:00
$transactionBookings = $this -> getDatabase () -> stock_log () -> where ( 'undone = 0 AND transaction_id = :1' , $transactionId ) -> orderBy ( 'id' , 'DESC' ) -> fetchAll ();
2019-12-19 12:48:36 -06:00
if ( count ( $transactionBookings ) === 0 )
{
throw new \Exception ( 'This transaction was not found or already undone' );
}
foreach ( $transactionBookings as $transactionBooking )
{
$this -> UndoBooking ( $transactionBooking -> id , true );
}
2020-08-31 20:40:31 +02:00
}
2020-12-20 20:58:22 +01:00
public function MergeProducts ( int $productIdToKeep , int $productIdToRemove )
{
if ( ! $this -> ProductExists ( $productIdToKeep ))
{
throw new \Exception ( '$productIdToKeep does not exist or is inactive' );
}
if ( ! $this -> ProductExists ( $productIdToRemove ))
{
throw new \Exception ( '$productIdToRemove does not exist or is inactive' );
}
if ( $productIdToKeep == $productIdToRemove )
{
throw new \Exception ( '$productIdToKeep cannot equal $productIdToRemove' );
}
$this -> getDatabaseService () -> GetDbConnectionRaw () -> beginTransaction ();
try
{
$productToKeep = $this -> getDatabase () -> products ( $productIdToKeep );
$productToRemove = $this -> getDatabase () -> products ( $productIdToRemove );
$conversion = $this -> getDatabase () -> quantity_unit_conversions_resolved () -> where ( 'product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3' , $productToRemove -> id , $productToRemove -> qu_id_stock , $productToKeep -> qu_id_stock ) -> fetch ();
$factor = 1.0 ;
if ( $conversion != null )
{
$factor = floatval ( $conversion -> factor );
}
$this -> getDatabaseService () -> ExecuteDbStatement ( 'UPDATE stock SET product_id = ' . $productIdToKeep . ', amount = amount * ' . $factor . ' WHERE product_id = ' . $productIdToRemove );
$this -> getDatabaseService () -> ExecuteDbStatement ( 'UPDATE stock_log SET product_id = ' . $productIdToKeep . ', amount = amount * ' . $factor . ' WHERE product_id = ' . $productIdToRemove );
$this -> getDatabaseService () -> ExecuteDbStatement ( 'UPDATE product_barcodes SET product_id = ' . $productIdToKeep . ' WHERE product_id = ' . $productIdToRemove );
$this -> getDatabaseService () -> ExecuteDbStatement ( 'UPDATE quantity_unit_conversions SET product_id = ' . $productIdToKeep . ' WHERE product_id = ' . $productIdToRemove );
$this -> getDatabaseService () -> ExecuteDbStatement ( 'UPDATE recipes_pos SET product_id = ' . $productIdToKeep . ', amount = amount * ' . $factor . ' WHERE product_id = ' . $productIdToRemove );
$this -> getDatabaseService () -> ExecuteDbStatement ( 'UPDATE recipes SET product_id = ' . $productIdToKeep . ' WHERE product_id = ' . $productIdToRemove );
$this -> getDatabaseService () -> ExecuteDbStatement ( 'UPDATE meal_plan SET product_id = ' . $productIdToKeep . ', product_amount = product_amount * ' . $factor . ' WHERE product_id = ' . $productIdToRemove );
$this -> getDatabaseService () -> ExecuteDbStatement ( 'UPDATE shopping_list SET product_id = ' . $productIdToKeep . ', amount = amount * ' . $factor . ' WHERE product_id = ' . $productIdToRemove );
$this -> getDatabaseService () -> ExecuteDbStatement ( 'DELETE FROM products WHERE id = ' . $productIdToRemove );
}
catch ( Exception $ex )
{
$this -> getDatabaseService () -> GetDbConnectionRaw () -> rollback ();
throw $ex ;
}
$this -> getDatabaseService () -> GetDbConnectionRaw () -> commit ();
}
2021-07-11 21:06:05 +02:00
public function CompactStockEntries ( $productId = null )
{
if ( $productId == null )
{
$splittedStockEntries = $this -> getDatabase () -> stock_splits ();
}
else
{
$splittedStockEntries = $this -> getDatabase () -> stock_splits () -> where ( 'product_id = :1' , $productId );
}
foreach ( $splittedStockEntries as $splittedStockEntry )
{
$this -> getDatabaseService () -> GetDbConnectionRaw () -> beginTransaction ();
try
{
$stockIds = explode ( ',' , $splittedStockEntry -> stock_id_group );
foreach ( $stockIds as $stockId )
{
if ( $stockId != $splittedStockEntry -> stock_id_to_keep )
{
$this -> getDatabaseService () -> ExecuteDbStatement ( 'UPDATE stock SET stock_id = \'' . $splittedStockEntry -> stock_id_to_keep . '\' WHERE stock_id = \'' . $stockId . '\'' );
$this -> getDatabaseService () -> ExecuteDbStatement ( 'UPDATE stock_log SET stock_id = \'' . $splittedStockEntry -> stock_id_to_keep . '\' WHERE stock_id = \'' . $stockId . '\'' );
}
}
$stockEntryIds = explode ( ',' , $splittedStockEntry -> id_group );
foreach ( $stockEntryIds as $stockEntryId )
{
if ( $stockEntryId != $splittedStockEntry -> id_to_keep )
{
$this -> getDatabaseService () -> ExecuteDbStatement ( 'DELETE FROM stock WHERE id = ' . $stockEntryId );
}
else
{
$this -> getDatabaseService () -> ExecuteDbStatement ( 'UPDATE stock SET amount = ' . $splittedStockEntry -> total_amount . ' WHERE id = ' . $splittedStockEntry -> id_to_keep );
}
}
}
catch ( Exception $ex )
{
$this -> getDatabaseService () -> GetDbConnectionRaw () -> rollback ();
throw $ex ;
}
$this -> getDatabaseService () -> GetDbConnectionRaw () -> commit ();
}
}
2020-08-31 20:40:31 +02:00
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 -> getDatabase () -> locations () -> fetchAll (), $this -> getDatabase () -> quantity_units () -> fetchAll ());
}
else
{
throw new \Exception ( " Plugin $pluginName was not found " );
}
}
private function LocationExists ( $locationId )
{
$locationRow = $this -> getDatabase () -> locations () -> where ( 'id = :1' , $locationId ) -> fetch ();
return $locationRow !== null ;
}
private function ProductExists ( $productId )
{
$productRow = $this -> getDatabase () -> products () -> where ( 'id = :1 and active = 1' , $productId ) -> fetch ();
return $productRow !== null ;
}
private function ShoppingListExists ( $listId )
{
$shoppingListRow = $this -> getDatabase () -> shopping_lists () -> where ( 'id = :1' , $listId ) -> fetch ();
return $shoppingListRow !== null ;
2019-12-19 12:48:36 -06:00
}
2017-04-16 23:11:03 +02:00
}