Compare commits

...

3 Commits

Author SHA1 Message Date
Bernd Bestel
41206cb901
Fixed changelog file name 2026-09-16 22:14:31 +02:00
Bernd Bestel
80f68d4e93
Removed obsolete code comment 2026-09-16 21:32:02 +02:00
Bernd Bestel
f1e92931c9
Support location_id in product open API endpoints (closes #2989) 2026-09-16 21:27:36 +02:00
5 changed files with 205 additions and 91 deletions

View File

@ -0,0 +1,57 @@
> ⚠️ xxxBREAKING CHANGESxxx
> ❗ xxxImportant upgrade informationXXX
> 💡 xxxMinor upgrade informationXXX
### New Feature: xxxx
- xxx
### Stock
- xxx
### Shopping list
- xxx
### Recipes
- xxx
### Meal plan
- xxx
### Chores
- xxx
### Calendar
- xxx
### Tasks
- xxx
### Batteries
- xxx
### Equipment
- xxx
### Userfields
- xxx
### General
- xxx
### API
- The API endpoints `/stock/products/{productId}/open` and `/stock/products/by-barcode/{barcode}/open` now support a new (optional) `location_id` request body parameter (to only open stock entries at that location)

View File

@ -555,6 +555,12 @@ class StockApiController extends BaseApiController
throw new \Exception('An amount is required'); throw new \Exception('An amount is required');
} }
$locationId = null;
if (array_key_exists('location_id', $requestBody) && !empty($requestBody['location_id']) && is_numeric($requestBody['location_id']))
{
$locationId = $requestBody['location_id'];
}
$specificStockEntryId = 'default'; $specificStockEntryId = 'default';
if (array_key_exists('stock_entry_id', $requestBody) && !empty($requestBody['stock_entry_id'])) if (array_key_exists('stock_entry_id', $requestBody) && !empty($requestBody['stock_entry_id']))
{ {
@ -568,7 +574,7 @@ class StockApiController extends BaseApiController
} }
$transactionId = null; $transactionId = null;
$transactionId = StockService::GetInstance()->OpenProduct($args['productId'], $requestBody['amount'], $specificStockEntryId, $transactionId, $allowSubproductSubstitution); $transactionId = StockService::GetInstance()->OpenProduct($args['productId'], $requestBody['amount'], $locationId, $specificStockEntryId, $transactionId, $allowSubproductSubstitution);
$args['transactionId'] = $transactionId; $args['transactionId'] = $transactionId;
return $this->StockTransactions($request, $response, $args); return $this->StockTransactions($request, $response, $args);
} }
@ -648,7 +654,7 @@ class StockApiController extends BaseApiController
$allowSubproductSubstitution = true; $allowSubproductSubstitution = true;
} }
return $this->FilteredApiResponse($response, StockService::GetInstance()->GetProductStockEntries($args['productId'], false, $allowSubproductSubstitution), $request->getQueryParams()); return $this->FilteredApiResponse($response, StockService::GetInstance()->GetProductStockEntries($args['productId'], null, false, $allowSubproductSubstitution), $request->getQueryParams());
} }
public function LocationStockEntries(Request $request, Response $response, array $args) public function LocationStockEntries(Request $request, Response $response, array $args)

View File

@ -2255,6 +2255,10 @@
"allow_subproduct_substitution": { "allow_subproduct_substitution": {
"type": "boolean", "type": "boolean",
"description": "`true` when any in stock sub product should be used when the given product is a parent product and currently not in stock" "description": "`true` when any in stock sub product should be used when the given product is a parent product and currently not in stock"
},
"location_id": {
"type": "integer",
"description": "A valid location id (if supplied, only stock at the given location is considered, if ommitted, stock of any location is considered)"
} }
}, },
"example": { "example": {
@ -2599,7 +2603,7 @@
}, },
"allow_subproduct_substitution": { "allow_subproduct_substitution": {
"type": "boolean", "type": "boolean",
"description": "`rue` when any in stock sub product should be used when the given product is a parent product and currently not in stock" "description": "`true` when any in stock sub product should be used when the given product is a parent product and currently not in stock"
} }
}, },
"example": { "example": {
@ -2822,7 +2826,11 @@
}, },
"allow_subproduct_substitution": { "allow_subproduct_substitution": {
"type": "boolean", "type": "boolean",
"description": "`rue` when any in stock sub product should be used when the given product is a parent product and currently not in stock" "description": "`true` when any in stock sub product should be used when the given product is a parent product and currently not in stock"
},
"location_id": {
"type": "integer",
"description": "A valid location id (if supplied, only stock at the given location is considered, if ommitted, stock of any location is considered)"
} }
}, },
"example": { "example": {

View File

@ -1,4 +1,4 @@
$('#save-consume-button').on('click', function(e) $('#save-consume-button').on('click', function (e)
{ {
e.preventDefault(); e.preventDefault();
@ -40,10 +40,10 @@
var bookingResponse = null; var bookingResponse = null;
Grocy.Api.Get('stock/products/' + jsonForm.product_id, Grocy.Api.Get('stock/products/' + jsonForm.product_id,
function(productDetails) function (productDetails)
{ {
Grocy.Api.Post(apiUrl, jsonData, Grocy.Api.Post(apiUrl, jsonData,
function(result) function (result)
{ {
if (BoolVal(Grocy.UserSettings.scan_mode_consume_enabled)) if (BoolVal(Grocy.UserSettings.scan_mode_consume_enabled))
{ {
@ -59,14 +59,14 @@
jsonDataBarcode.product_id = jsonForm.product_id; jsonDataBarcode.product_id = jsonForm.product_id;
Grocy.Api.Post('objects/product_barcodes', jsonDataBarcode, Grocy.Api.Post('objects/product_barcodes', jsonDataBarcode,
function(result) function (result)
{ {
$("#flow-info-InplaceAddBarcodeToExistingProduct").addClass("d-none"); $("#flow-info-InplaceAddBarcodeToExistingProduct").addClass("d-none");
$('#barcode-lookup-disabled-hint').addClass('d-none'); $('#barcode-lookup-disabled-hint').addClass('d-none');
$('#barcode-lookup-hint').removeClass('d-none'); $('#barcode-lookup-hint').removeClass('d-none');
window.history.replaceState({}, document.title, U("/consume")); window.history.replaceState({}, document.title, U("/consume"));
}, },
function(xhr) function (xhr)
{ {
Grocy.FrontendHelpers.EndUiBusy("consume-form"); Grocy.FrontendHelpers.EndUiBusy("consume-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response); Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
@ -130,7 +130,7 @@
$("#consume-exact-amount-group").addClass("d-none"); $("#consume-exact-amount-group").addClass("d-none");
} }
}, },
function(xhr) function (xhr)
{ {
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response); Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
Grocy.FrontendHelpers.EndUiBusy("consume-form"); Grocy.FrontendHelpers.EndUiBusy("consume-form");
@ -138,7 +138,7 @@
} }
); );
}, },
function(xhr) function (xhr)
{ {
Grocy.FrontendHelpers.EndUiBusy("consume-form"); Grocy.FrontendHelpers.EndUiBusy("consume-form");
console.error(xhr); console.error(xhr);
@ -146,7 +146,7 @@
); );
}); });
$('#save-mark-as-open-button').on('click', function(e) $('#save-mark-as-open-button').on('click', function (e)
{ {
e.preventDefault(); e.preventDefault();
@ -168,6 +168,7 @@ $('#save-mark-as-open-button').on('click', function(e)
jsonData = {}; jsonData = {};
jsonData.amount = jsonForm.amount; jsonData.amount = jsonForm.amount;
jsonData.allow_subproduct_substitution = true; jsonData.allow_subproduct_substitution = true;
jsonData.location_id = jsonForm.location_id;
if ($("#use_specific_stock_entry").is(":checked")) if ($("#use_specific_stock_entry").is(":checked"))
{ {
@ -175,10 +176,10 @@ $('#save-mark-as-open-button').on('click', function(e)
} }
Grocy.Api.Get('stock/products/' + jsonForm.product_id, Grocy.Api.Get('stock/products/' + jsonForm.product_id,
function(productDetails) function (productDetails)
{ {
Grocy.Api.Post(apiUrl, jsonData, Grocy.Api.Post(apiUrl, jsonData,
function(result) function (result)
{ {
$("#specific_stock_entry").find("option").remove().end().append("<option></option>"); $("#specific_stock_entry").find("option").remove().end().append("<option></option>");
if ($("#use_specific_stock_entry").is(":checked")) if ($("#use_specific_stock_entry").is(":checked"))
@ -206,16 +207,17 @@ $('#save-mark-as-open-button').on('click', function(e)
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
Grocy.Components.ProductPicker.Clear(); Grocy.Components.ProductPicker.Clear();
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.GetInputElement().focus();
Grocy.Components.ProductCard.Refresh(jsonForm.product_id);
Grocy.FrontendHelpers.ValidateForm('consume-form'); Grocy.FrontendHelpers.ValidateForm('consume-form');
}, },
function(xhr) function (xhr)
{ {
Grocy.FrontendHelpers.EndUiBusy("consume-form"); Grocy.FrontendHelpers.EndUiBusy("consume-form");
console.error(xhr); console.error(xhr);
} }
); );
}, },
function(xhr) function (xhr)
{ {
Grocy.FrontendHelpers.EndUiBusy("consume-form"); Grocy.FrontendHelpers.EndUiBusy("consume-form");
console.error(xhr); console.error(xhr);
@ -223,7 +225,8 @@ $('#save-mark-as-open-button').on('click', function(e)
); );
}); });
var sumValue = 0; var sumValue = 0;
$("#location_id").on('change', function(e) var sumValueOpened = 0;
$("#location_id").on('change', function (e)
{ {
var locationId = $(e.target).val(); var locationId = $(e.target).val();
$("#specific_stock_entry").find("option").remove().end().append("<option></option>"); $("#specific_stock_entry").find("option").remove().end().append("<option></option>");
@ -245,12 +248,12 @@ $("#location_id").on('change', function(e)
if (gc.length == 4) if (gc.length == 4)
{ {
Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries?query[]=stock_id=' + gc[3], Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries?query[]=stock_id=' + gc[3],
function(stockEntries) function (stockEntries)
{ {
OnLocationChange(stockEntries[0].location_id, gc[3]); OnLocationChange(stockEntries[0].location_id, gc[3]);
$('#display_amount').val(stockEntries[0].amount); $('#display_amount').val(stockEntries[0].amount);
}, },
function(xhr) function (xhr)
{ {
console.error(xhr); console.error(xhr);
} }
@ -271,6 +274,7 @@ $("#location_id").on('change', function(e)
function OnLocationChange(locationId, stockId) function OnLocationChange(locationId, stockId)
{ {
sumValue = 0; sumValue = 0;
sumValueOpened = 0;
if (locationId) if (locationId)
{ {
@ -280,7 +284,7 @@ function OnLocationChange(locationId, stockId)
} }
Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries?include_sub_products=true', Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries?include_sub_products=true',
function(stockEntries) function (stockEntries)
{ {
stockEntries.forEach(stockEntry => stockEntries.forEach(stockEntry =>
{ {
@ -307,6 +311,11 @@ function OnLocationChange(locationId, stockId)
sumValue = sumValue + (stockEntry.amount || 0); sumValue = sumValue + (stockEntry.amount || 0);
if (BoolVal(stockEntry.open))
{
sumValueOpened = sumValueOpened + (stockEntry.amount || 0);
}
if (stockEntry.stock_id == stockId) if (stockEntry.stock_id == stockId)
{ {
$("#use_specific_stock_entry").click(); $("#use_specific_stock_entry").click();
@ -315,13 +324,15 @@ function OnLocationChange(locationId, stockId)
} }
}); });
Grocy.Api.Get('stock/products/' + Grocy.Components.ProductPicker.GetValue(), Grocy.Api.Get('stock/products/' + Grocy.Components.ProductPicker.GetValue(),
function(productDetails) function (productDetails)
{ {
current_productDetails = productDetails; current_productDetails = productDetails;
RefreshForm(); RefreshForm();
}, },
function(xhr) function (xhr)
{ {
console.error(xhr); console.error(xhr);
} }
@ -332,7 +343,7 @@ function OnLocationChange(locationId, stockId)
ScanModeSubmit(); ScanModeSubmit();
} }
}, },
function(xhr) function (xhr)
{ {
console.error(xhr); console.error(xhr);
} }
@ -340,7 +351,7 @@ function OnLocationChange(locationId, stockId)
} }
} }
Grocy.Components.ProductPicker.GetPicker().on('change', function(e) Grocy.Components.ProductPicker.GetPicker().on('change', function (e)
{ {
if (BoolVal(Grocy.UserSettings.scan_mode_consume_enabled)) if (BoolVal(Grocy.UserSettings.scan_mode_consume_enabled))
{ {
@ -361,7 +372,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
Grocy.Components.ProductCard.Refresh(productId); Grocy.Components.ProductCard.Refresh(productId);
Grocy.Api.Get('stock/products/' + productId, Grocy.Api.Get('stock/products/' + productId,
function(productDetails) function (productDetails)
{ {
current_productDetails = productDetails; current_productDetails = productDetails;
@ -386,7 +397,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
$("#location_id").find("option").remove().end().append("<option></option>"); $("#location_id").find("option").remove().end().append("<option></option>");
Grocy.Api.Get("stock/products/" + productId + '/locations?include_sub_products=true', Grocy.Api.Get("stock/products/" + productId + '/locations?include_sub_products=true',
function(stockLocations) function (stockLocations)
{ {
var setDefault = 0; var setDefault = 0;
var stockAmountAtDefaultLocation = 0; var stockAmountAtDefaultLocation = 0;
@ -436,7 +447,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
if (document.getElementById("product_id").getAttribute("barcode") != "null") if (document.getElementById("product_id").getAttribute("barcode") != "null")
{ {
Grocy.Api.Get('objects/product_barcodes?query[]=barcode=' + document.getElementById("product_id").getAttribute("barcode"), Grocy.Api.Get('objects/product_barcodes?query[]=barcode=' + document.getElementById("product_id").getAttribute("barcode"),
function(barcodeResult) function (barcodeResult)
{ {
if (barcodeResult != null) if (barcodeResult != null)
{ {
@ -462,14 +473,14 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
} }
} }
}, },
function(xhr) function (xhr)
{ {
console.error(xhr); console.error(xhr);
} }
); );
} }
}, },
function(xhr) function (xhr)
{ {
console.error(xhr); console.error(xhr);
} }
@ -489,21 +500,12 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
Grocy.Components.ProductPicker.HideCustomError(); Grocy.Components.ProductPicker.HideCustomError();
Grocy.FrontendHelpers.ValidateForm('consume-form'); Grocy.FrontendHelpers.ValidateForm('consume-form');
setTimeout(function() setTimeout(function ()
{ {
$('#display_amount').focus(); $('#display_amount').focus();
}, Grocy.FormFocusDelay); }, Grocy.FormFocusDelay);
if (productDetails.stock_amount == productDetails.stock_amount_opened || productDetails.product.enable_tare_weight_handling == 1 || productDetails.product.disable_open == 1)
{
$("#save-mark-as-open-button").addClass("disabled");
}
else
{
$("#save-mark-as-open-button").removeClass("disabled");
}
}, },
function(xhr) function (xhr)
{ {
console.error(xhr); console.error(xhr);
} }
@ -515,28 +517,44 @@ $('#display_amount').val(Grocy.UserSettings.stock_default_consume_amount);
$(".input-group-productamountpicker").trigger("change"); $(".input-group-productamountpicker").trigger("change");
Grocy.FrontendHelpers.ValidateForm('consume-form'); Grocy.FrontendHelpers.ValidateForm('consume-form');
$('#display_amount').on('focus', function(e) $('#display_amount').on('focus', function (e)
{ {
$(this).select(); $(this).select();
}); });
$('#price').on('focus', function(e) $('#price').on('focus', function (e)
{ {
$(this).select(); $(this).select();
}); });
$('#consume-form input').keyup(function (event)
$('#consume-form input').keyup(function(event)
{ {
Grocy.FrontendHelpers.ValidateForm('consume-form'); Grocy.FrontendHelpers.ValidateForm('consume-form');
}); });
$('#consume-form select').change(function(event) $('#consume-form select').change(function (event)
{ {
Grocy.FrontendHelpers.ValidateForm('consume-form'); Grocy.FrontendHelpers.ValidateForm('consume-form');
}); });
$('#consume-form input').keydown(function(event) $("#display_amount").on("keyup", function (e)
{
if (!current_productDetails)
{
return;
}
if ($("#display_amount").val() > sumValue - sumValueOpened || current_productDetails.product.enable_tare_weight_handling == 1 || current_productDetails.product.disable_open == 1)
{
$("#save-mark-as-open-button").addClass("disabled");
}
else
{
$("#save-mark-as-open-button").removeClass("disabled");
}
});
$('#consume-form input').keydown(function (event)
{ {
if (event.keyCode === 13) // Enter if (event.keyCode === 13) // Enter
{ {
@ -553,28 +571,45 @@ $('#consume-form input').keydown(function(event)
} }
}); });
$("#specific_stock_entry").on("change", function(e) $("#specific_stock_entry").on("change", function (e)
{ {
if ($(e.target).val() == "") if ($(e.target).val() == "")
{ {
sumValue = 0; sumValue = 0;
sumValueOpened = 0;
Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries?include_sub_products=true', Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries?include_sub_products=true',
function(stockEntries) function (stockEntries)
{ {
stockEntries.forEach(stockEntry => stockEntries.forEach(stockEntry =>
{ {
if (stockEntry.location_id == $("#location_id").val() || stockEntry.location_id == "") if (stockEntry.location_id == $("#location_id").val() || stockEntry.location_id == "")
{ {
sumValue = sumValue + stockEntry.amount_aggregated; sumValue = sumValue + (stockEntry.amount || 0);
if (BoolVal(stockEntry.open))
{
sumValueOpened = sumValueOpened + (stockEntry.amount || 0);
}
} }
}); });
$("#display_amount").attr("max", sumValue.toFixed(Grocy.UserSettings.stock_decimal_places_amounts)); $("#display_amount").attr("max", sumValue.toFixed(Grocy.UserSettings.stock_decimal_places_amounts));
if (sumValue == 0) if (sumValue == 0)
{ {
$("#display_amount").parent().find(".invalid-feedback").text(__t('There are no units available at this location')); $("#display_amount").parent().find(".invalid-feedback").text(__t('There are no units available at this location'));
} }
if (sumValue == sumValueOpened || current_productDetails.product.enable_tare_weight_handling == 1 || current_productDetails.product.disable_open == 1)
{
$("#save-mark-as-open-button").addClass("disabled");
}
else
{
$("#save-mark-as-open-button").removeClass("disabled");
}
}, },
function(xhr) function (xhr)
{ {
console.error(xhr); console.error(xhr);
} }
@ -586,7 +621,7 @@ $("#specific_stock_entry").on("change", function(e)
} }
}); });
$("#use_specific_stock_entry").on("change", function() $("#use_specific_stock_entry").on("change", function ()
{ {
var value = $(this).is(":checked"); var value = $(this).is(":checked");
@ -605,7 +640,7 @@ $("#use_specific_stock_entry").on("change", function()
Grocy.FrontendHelpers.ValidateForm("consume-form"); Grocy.FrontendHelpers.ValidateForm("consume-form");
}); });
$("#qu_id").on("change", function() $("#qu_id").on("change", function ()
{ {
RefreshForm(); RefreshForm();
}); });
@ -613,11 +648,11 @@ $("#qu_id").on("change", function()
function UndoStockBooking(bookingId) function UndoStockBooking(bookingId)
{ {
Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {}, Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {},
function(result) function (result)
{ {
toastr.success(__t("Booking successfully undone")); toastr.success(__t("Booking successfully undone"));
}, },
function(xhr) function (xhr)
{ {
console.error(xhr); console.error(xhr);
} }
@ -627,11 +662,11 @@ function UndoStockBooking(bookingId)
function UndoStockTransaction(transactionId) function UndoStockTransaction(transactionId)
{ {
Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', {}, Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', {},
function(result) function (result)
{ {
toastr.success(__t("Transaction successfully undone")); toastr.success(__t("Transaction successfully undone"));
}, },
function(xhr) function (xhr)
{ {
console.error(xhr); console.error(xhr);
} }
@ -657,12 +692,12 @@ if (GetUriParam("embedded") !== undefined)
} }
// Default input field // Default input field
setTimeout(function() setTimeout(function ()
{ {
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.GetInputElement().focus();
}, Grocy.FormFocusDelay); }, Grocy.FormFocusDelay);
$(document).on("change", "#scan-mode", function(e) $(document).on("change", "#scan-mode", function (e)
{ {
if ($(this).prop("checked")) if ($(this).prop("checked"))
{ {
@ -670,7 +705,7 @@ $(document).on("change", "#scan-mode", function(e)
} }
}); });
$("#scan-mode-button").on("click", function(e) $("#scan-mode-button").on("click", function (e)
{ {
$("#scan-mode").click(); $("#scan-mode").click();
$("#scan-mode-button").toggleClass("btn-success").toggleClass("btn-danger"); $("#scan-mode-button").toggleClass("btn-success").toggleClass("btn-danger");
@ -727,6 +762,15 @@ function RefreshForm()
$("#display_amount").removeAttr("max"); $("#display_amount").removeAttr("max");
} }
if (sumValue == sumValueOpened || productDetails.product.enable_tare_weight_handling == 1 || productDetails.product.disable_open == 1)
{
$("#save-mark-as-open-button").addClass("disabled");
}
else
{
$("#save-mark-as-open-button").removeClass("disabled");
}
Grocy.FrontendHelpers.ValidateForm("consume-form"); Grocy.FrontendHelpers.ValidateForm("consume-form");
} }

View File

@ -400,26 +400,24 @@ class StockService extends BaseService
if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION) if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION)
{ {
if ($locationId === null) $potentialStockEntries = $this->GetProductStockEntries($productId, $locationId, false, $allowSubproductSubstitution);
{
// Consume from any location
$potentialStockEntries = $this->GetProductStockEntries($productId, false, $allowSubproductSubstitution);
}
else
{
// Consume only from the supplied location
$potentialStockEntries = $this->GetProductStockEntriesForLocation($productId, $locationId, false, $allowSubproductSubstitution);
}
if ($specificStockEntryId !== 'default') if ($specificStockEntryId !== 'default')
{ {
$potentialStockEntries = FindAllObjectsInArrayByPropertyValue($potentialStockEntries, 'stock_id', $specificStockEntryId); $potentialStockEntries = FindAllObjectsInArrayByPropertyValue($potentialStockEntries, 'stock_id', $specificStockEntryId);
} }
$productStockAmount = $productDetails->stock_amount_aggregated; if ($locationId === null)
if (round($amount, 2) > round($productStockAmount, 2))
{ {
throw new \Exception('Amount to be consumed cannot be > current stock amount (if supplied, at the desired location)'); $productStockAmountTotal = $this->DB->stock()->where('product_id = :1', $productId)->sum('amount');
}
else
{
$productStockAmountTotal = $this->DB->stock()->where('product_id = :1 AND location_id = :2', $productId, $locationId)->sum('amount');
}
if ($amount > $productStockAmountTotal)
{
throw new \Exception('Amount to be consumed cannot be > current unopened stock amount (if supplied, at the desired location)');
} }
if ($transactionId === null) if ($transactionId === null)
@ -865,7 +863,7 @@ class StockService extends BaseService
return $returnData; return $returnData;
} }
public function GetProductStockEntries(int $productId, $excludeOpened = false, $allowSubproductSubstitution = false) public function GetProductStockEntries(int $productId, $locationId = null, $excludeOpened = false, $allowSubproductSubstitution = false)
{ {
$sqlWhereProductId = 'product_id = ' . $productId; $sqlWhereProductId = 'product_id = ' . $productId;
if ($allowSubproductSubstitution) if ($allowSubproductSubstitution)
@ -879,7 +877,13 @@ class StockService extends BaseService
$sqlWhereAndOpen = 'AND open = 0'; $sqlWhereAndOpen = 'AND open = 0';
} }
return $this->DB->stock_next_use()->where($sqlWhereProductId . ' ' . $sqlWhereAndOpen); $sqlWhereAndLocationId = '';
if ($locationId !== null)
{
$sqlWhereAndLocationId = 'AND location_id = ' . $locationId;
}
return $this->DB->stock_next_use()->where($sqlWhereProductId . ' ' . $sqlWhereAndOpen . ' ' . $sqlWhereAndLocationId);
} }
public function GetLocationStockEntries($locationId) public function GetLocationStockEntries($locationId)
@ -892,12 +896,6 @@ class StockService extends BaseService
return $this->DB->stock()->where('location_id', $locationId); return $this->DB->stock()->where('location_id', $locationId);
} }
public function GetProductStockEntriesForLocation($productId, $locationId, $excludeOpened = false, $allowSubproductSubstitution = false)
{
$stockEntries = $this->GetProductStockEntries($productId, $excludeOpened, $allowSubproductSubstitution);
return FindAllObjectsInArrayByPropertyValue($stockEntries, 'location_id', $locationId);
}
public function GetProductStockLocations(int $productId, $allowSubproductSubstitution = false) public function GetProductStockLocations(int $productId, $allowSubproductSubstitution = false)
{ {
$sqlWhereProductId = 'product_id = ' . $productId; $sqlWhereProductId = 'product_id = ' . $productId;
@ -978,7 +976,7 @@ class StockService extends BaseService
return null; return null;
} }
public function OpenProduct(int $productId, float $amount, $specificStockEntryId = 'default', &$transactionId = null, $allowSubproductSubstitution = false) public function OpenProduct(int $productId, float $amount, $locationId = null, $specificStockEntryId = 'default', &$transactionId = null, $allowSubproductSubstitution = false)
{ {
if (!$this->ProductExists($productId)) if (!$this->ProductExists($productId))
{ {
@ -993,17 +991,24 @@ class StockService extends BaseService
} }
$productDetails = (object)$this->GetProductDetails($productId); $productDetails = (object)$this->GetProductDetails($productId);
$productStockAmountUnopened = $productDetails->stock_amount_aggregated - $productDetails->stock_amount_opened_aggregated; $potentialStockEntries = $this->GetProductStockEntries($productId, $locationId, true, $allowSubproductSubstitution);
$potentialStockEntries = $this->GetProductStockEntries($productId, true, $allowSubproductSubstitution);
if ($product->enable_tare_weight_handling == 1) if ($product->enable_tare_weight_handling == 1)
{ {
throw new \Exception('Opening tare weight handling enabled products is not supported'); throw new \Exception('Opening tare weight handling enabled products is not supported');
} }
if ($locationId === null)
{
$productStockAmountUnopened = $this->DB->stock()->where('product_id = :1 AND open = 0', $productId)->sum('amount');
}
else
{
$productStockAmountUnopened = $this->DB->stock()->where('product_id = :1 AND open = 0 AND location_id = :2', $productId, $locationId)->sum('amount');
}
if ($amount > $productStockAmountUnopened) if ($amount > $productStockAmountUnopened)
{ {
throw new \Exception('Amount to be opened cannot be > current unopened stock amount'); throw new \Exception('Amount to be opened cannot be > current unopened stock amount (if supplied, at the desired location)');
} }
if ($specificStockEntryId !== 'default') if ($specificStockEntryId !== 'default')
@ -1290,16 +1295,10 @@ class StockService extends BaseService
{ {
// Hard fail for now, as we not yet support transferring tare weight enabled products // 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'); throw new \Exception('Transferring tare weight enabled products is not yet possible');
if ($amount < $productDetails->product->tare_weight)
{
throw new \Exception('The amount cannot be lower than the defined tare weight');
}
$amount = abs($amount - $productDetails->stock_amount - $productDetails->product->tare_weight);
} }
$productStockAmountAtFromLocation = $this->DB->stock()->where('product_id = :1 AND location_id = :2', $productId, $locationIdFrom)->sum('amount'); $productStockAmountAtFromLocation = $this->DB->stock()->where('product_id = :1 AND location_id = :2', $productId, $locationIdFrom)->sum('amount');
$potentialStockEntriesAtFromLocation = $this->GetProductStockEntriesForLocation($productId, $locationIdFrom); $potentialStockEntriesAtFromLocation = $this->GetProductStockEntries($productId, $locationIdFrom);
if ($amount > $productStockAmountAtFromLocation) if ($amount > $productStockAmountAtFromLocation)
{ {