Implemented tare weight handling (closes #132)

This commit is contained in:
Bernd Bestel
2019-03-05 17:51:50 +01:00
parent 90291fdbca
commit 8504429f5f
21 changed files with 221 additions and 84 deletions

View File

@@ -27,7 +27,7 @@ class BatteriesApiController extends BaseApiController
}
$chargeCycleId = $this->BatteriesService->TrackChargeCycle($args['batteryId'], $trackedTime);
return $this->ApiResponse(array('charge_cycle_id' => $chargeCycleId));
return $this->ApiResponse($this->Database->battery_charge_cycles($chargeCycleId));
}
catch (\Exception $ex)
{

View File

@@ -33,7 +33,7 @@ class ChoresApiController extends BaseApiController
}
$choreExecutionId = $this->ChoresService->TrackChore($args['choreId'], $trackedTime, $doneBy);
return $this->ApiResponse(array('chore_execution_id' => $choreExecutionId));
return $this->ApiResponse($this->Database->chores_log($choreExecutionId));
}
catch (\Exception $ex)
{

View File

@@ -79,7 +79,7 @@ class StockApiController extends BaseApiController
}
$bookingId = $this->StockService->AddProduct($args['productId'], $requestBody['amount'], $bestBeforeDate, $transactionType, date('Y-m-d'), $price, $locationId);
return $this->ApiResponse(array('booking_id' => $bookingId));
return $this->ApiResponse($this->Database->stock_log($bookingId));
}
catch (\Exception $ex)
{
@@ -128,7 +128,7 @@ class StockApiController extends BaseApiController
}
$bookingId = $this->StockService->ConsumeProduct($args['productId'], $requestBody['amount'], $spoiled, $transactionType, $specificStockEntryId, $recipeId);
return $this->ApiResponse(array('booking_id' => $bookingId));
return $this->ApiResponse($this->Database->stock_log($bookingId));
}
catch (\Exception $ex)
{
@@ -159,7 +159,7 @@ class StockApiController extends BaseApiController
}
$bookingId = $this->StockService->InventoryProduct($args['productId'], $requestBody['new_amount'], $bestBeforeDate);
return $this->ApiResponse(array('booking_id' => $bookingId));
return $this->ApiResponse($this->Database->stock_log($bookingId));
}
catch (\Exception $ex)
{
@@ -190,7 +190,7 @@ class StockApiController extends BaseApiController
}
$bookingId = $this->StockService->OpenProduct($args['productId'], $requestBody['amount'], $specificStockEntryId);
return $this->ApiResponse(array('booking_id' => $bookingId));
return $this->ApiResponse($this->Database->stock_log($bookingId));
}
catch (\Exception $ex)
{

View File

@@ -1022,7 +1022,8 @@
"properties": {
"amount": {
"type": "number",
"format": "double"
"format": "double",
"description": "The amount to add - please note that when tare weight handling for the product is enabled, this needs to be the amount including the container weight (gross), the amount to be posted will be automatically calculated based on what is in stock and the defined tare weight"
},
"best_before_date": {
"type": "string",
@@ -1059,7 +1060,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StockBookingResponse"
"$ref": "#/components/schemas/StockLogEntry"
}
}
}
@@ -1103,7 +1104,7 @@
"properties": {
"amount": {
"type": "double",
"description": "The amount to remove"
"description": "The amount to remove - please note that when tare weight handling for the product is enabled, this needs to be the amount including the container weight (gross), the amount to be posted will be automatically calculated based on what is in stock and the defined tare weight"
},
"transaction_type": {
"$ref": "#/components/internalSchemas/StockTransactionType"
@@ -1137,7 +1138,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StockBookingResponse"
"$ref": "#/components/schemas/StockLogEntry"
}
}
}
@@ -1181,7 +1182,7 @@
"properties": {
"new_amount": {
"type": "integer",
"description": "The new current amount for the given product"
"description": "The new current amount for the given product - please note that when tare weight handling for the product is enabled, this needs to be the amount including the container weight (gross), the amount to be posted will be automatically calculated based on what is in stock and the defined tare weight"
},
"best_before_date": {
"type": "string",
@@ -1199,7 +1200,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StockBookingResponse"
"$ref": "#/components/schemas/StockLogEntry"
}
}
}
@@ -1263,7 +1264,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StockBookingResponse"
"$ref": "#/components/schemas/StockLogEntry"
}
}
}
@@ -1568,12 +1569,7 @@
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"chore_execution_id": {
"type": "integer"
}
}
"$ref": "#/components/schemas/ChoreLogEntry"
}
}
}
@@ -1729,12 +1725,7 @@
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"charge_cycle_id": {
"type": "integer"
}
}
"$ref": "#/components/schemas/BatteryChargeCycleEntry"
}
}
}
@@ -2345,7 +2336,7 @@
}
}
},
"BatteryChargeCycle": {
"BatteryChargeCycleEntry": {
"type": "object",
"properties": {
"id": {
@@ -2462,14 +2453,6 @@
"error_message": "The error message..."
}
},
"StockBookingResponse": {
"type": "object",
"properties": {
"booking_id": {
"type": "integer"
}
}
},
"CurrentStockResponse": {
"type": "object",
"properties": {

View File

@@ -335,5 +335,11 @@ return array(
'This is for statistical purposes only' => 'This is for statistical purposes only',
'You have to select a recipe' => 'You have to select a recipe',
'Key type' => 'Key type',
'Export as iCal' => 'Export as iCal'
'Export as iCal' => 'Export as iCal',
'Allow partial units in stock' => 'Allow partial units in stock',
'Enable tare weight handling' => 'Enable tare weight handling',
'This is useful e.g. for flour in jars - on purchase/consume/inventory you always weigh the whole jar, the amount to be posted is then automatically calculated based on what is in stock and the tare weight defined below' => 'This is useful e.g. for flour in jars - on purchase/consume/inventory you always weigh the whole jar, the amount to be posted is then automatically calculated based on what is in stock and the tare weight defined below',
'Tare weight' => 'Tare weight',
'Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated' => 'Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated',
'You have to select a location' => 'You have to select a location'
);

5
migrations/0057.sql Normal file
View File

@@ -0,0 +1,5 @@
ALTER TABLE products
ADD enable_tare_weight_handling TINYINT NOT NULL DEFAULT 0;
ALTER TABLE products
ADD tare_weight REAL NOT NULL DEFAULT 0;

View File

@@ -502,7 +502,7 @@ $("#about-dialog-link").on("click", function()
});
});
$(".local-number-format[data-format='currency']").each(function ()
$(".locale-number-format[data-format='currency']").each(function ()
{
$(this).text(parseFloat($(this).text()).toLocaleString(undefined, { minimumFractionDigits: 2 }));
$(this).text(parseFloat($(this).text()).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency }));
});

View File

@@ -12,7 +12,7 @@
function(result)
{
Grocy.FrontendHelpers.EndUiBusy("batterytracking-form");
toastr.success(L('Tracked charge cycle of battery #1 on #2', batteryDetails.battery.name, $('#tracked_time').find('input').val()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoChargeCycle(' + result.charge_cycle_id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>');
toastr.success(L('Tracked charge cycle of battery #1 on #2', batteryDetails.battery.name, $('#tracked_time').find('input').val()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoChargeCycle(' + result.id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>');
$('#battery_id').val('');
$('#battery_id_text_input').focus();

View File

@@ -12,7 +12,7 @@
function(result)
{
Grocy.FrontendHelpers.EndUiBusy("choretracking-form");
toastr.success(L('Tracked execution of chore #1 on #2', choreDetails.chore.name, Grocy.Components.DateTimePicker.GetValue()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoChoreExecution(' + result.chore_execution_id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>');
toastr.success(L('Tracked execution of chore #1 on #2', choreDetails.chore.name, Grocy.Components.DateTimePicker.GetValue()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoChoreExecution(' + result.id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>');
$('#chore_id').val('');
$('#chore_id_text_input').focus();

View File

@@ -39,9 +39,14 @@
}
Grocy.FrontendHelpers.EndUiBusy("consume-form");
toastr.success(L('Removed #1 #2 of #3 from stock', jsonForm.amount, Pluralize(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + result.booking_id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>');
toastr.success(L('Removed #1 #2 of #3 from stock', Math.abs(result.amount), Pluralize(Math.abs(result.amount), productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + result.id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>');
$("#amount").attr("min", "1");
$("#amount").attr("max", "999999");
$("#amount").attr("step", "1");
$("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '1'));
$('#amount').val(1);
$("#tare-weight-handling-info").addClass("d-none");
Grocy.Components.ProductPicker.Clear();
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_RECIPES)
{
@@ -100,7 +105,7 @@ $('#save-mark-as-open-button').on('click', function(e)
}
Grocy.FrontendHelpers.EndUiBusy("consume-form");
toastr.success(L('Marked #1 #2 of #3 as opened', jsonForm.amount, Pluralize(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + result.booking_id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>');
toastr.success(L('Marked #1 #2 of #3 as opened', jsonForm.amount, Pluralize(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + result.id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>');
$('#amount').val(1);
Grocy.Components.ProductPicker.Clear();
@@ -146,13 +151,25 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
{
$("#amount").attr("min", "0.01");
$("#amount").attr("step", "0.01");
$("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', 0.01.toLocaleString()));
$("#amount").parent().find(".invalid-feedback").text(L('The amount must be between #1 and #2', 0.01.toLocaleString(), parseFloat(productDetails.stock_amount).toLocaleString()));
}
else
{
$("#amount").attr("min", "1");
$("#amount").attr("step", "1");
$("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '1'));
$("#amount").parent().find(".invalid-feedback").text(L('The amount must be between #1 and #2', "1", parseFloat(productDetails.stock_amount).toLocaleString()));
}
if (productDetails.product.enable_tare_weight_handling == 1)
{
$("#amount").attr("min", productDetails.product.tare_weight);
$('#amount').attr('max', parseFloat(productDetails.stock_amount) + parseFloat(productDetails.product.tare_weight));
$("#amount").parent().find(".invalid-feedback").text(L('The amount must be between #1 and #2', parseFloat(productDetails.product.tare_weight).toLocaleString(), (parseFloat(productDetails.stock_amount) + parseFloat(productDetails.product.tare_weight)).toLocaleString()));
$("#tare-weight-handling-info").removeClass("d-none");
}
else
{
$("#tare-weight-handling-info").addClass("d-none");
}
if ((productDetails.stock_amount || 0) === 0)

View File

@@ -6,7 +6,7 @@
Grocy.FrontendHelpers.BeginUiBusy("inventory-form");
Grocy.Api.Get('stock/products/' + jsonForm.product_id,
function (productDetails)
function(productDetails)
{
var jsonData = { };
jsonData.new_amount = jsonForm.new_amount;
@@ -38,7 +38,7 @@
}
Grocy.FrontendHelpers.EndUiBusy("inventory-form");
toastr.success(L('Stock amount of #1 is now #2 #3', productDetails.product.name, jsonForm.new_amount, Pluralize(jsonForm.new_amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + result.booking_id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>');
toastr.success(L('Stock amount of #1 is now #2 #3', productDetails.product.name, productDetails.stock_amount, Pluralize(productDetails.stock_amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + result.id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>');
if (addBarcode !== undefined)
{
@@ -47,6 +47,10 @@
else
{
$('#inventory-change-info').addClass('d-none');
$("#tare-weight-handling-info").addClass("d-none");
$("#new_amount").attr("min", "0");
$("#new_amount").attr("step", "1");
$("#new_amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '0'));
$('#new_amount').val('');
Grocy.Components.DateTimePicker.Clear();
Grocy.Components.ProductPicker.SetValue('');
@@ -91,9 +95,20 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
}
else
{
$("#new_amount").attr("min", "1");
$("#new_amount").attr("min", "0");
$("#new_amount").attr("step", "1");
$("#new_amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '1'));
$("#new_amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '0'));
}
if (productDetails.product.enable_tare_weight_handling == 1)
{
$("#new_amount").attr("min", productDetails.product.tare_weight);
$("#new_amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', parseFloat(productDetails.product.tare_weight).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: 2 })));
$("#tare-weight-handling-info").removeClass("d-none");
}
else
{
$("#tare-weight-handling-info").addClass("d-none");
}
$('#new_amount').focus();
@@ -177,26 +192,31 @@ $('#new_amount').on('keyup', function(e)
Grocy.Api.Get('stock/products/' + productId,
function(productDetails)
{
var productStockAmount = parseInt(productDetails.stock_amount || '0');
var productStockAmount = parseFloat(productDetails.stock_amount || parseFloat('0'));
if (newAmount > productStockAmount)
var containerWeight = parseFloat("0");
if (productDetails.product.enable_tare_weight_handling == 1)
{
var amountToAdd = newAmount - productDetails.stock_amount;
$('#inventory-change-info').text(L('This means #1 will be added to stock', amountToAdd.toString() + ' ' + productDetails.quantity_unit_stock.name));
$('#inventory-change-info').removeClass('d-none');
Grocy.Components.DateTimePicker.GetInputElement().attr('required', '');
containerWeight = parseFloat(productDetails.product.tare_weight);
}
else if (newAmount < productStockAmount)
{
var amountToRemove = productStockAmount - newAmount;
$('#inventory-change-info').text(L('This means #1 will be removed from stock', amountToRemove.toString() + ' ' + productDetails.quantity_unit_stock.name));
$('#inventory-change-info').removeClass('d-none');
Grocy.Components.DateTimePicker.GetInputElement().removeAttr('required');
}
else
var estimatedBookingAmount = Math.abs(newAmount - productStockAmount - containerWeight);
$('#inventory-change-info').removeClass('d-none');
if (productDetails.product.enable_tare_weight_handling == 1 && newAmount < containerWeight)
{
$('#inventory-change-info').addClass('d-none');
}
else if (newAmount > productStockAmount + containerWeight)
{
$('#inventory-change-info').text(L('This means #1 will be added to stock', estimatedBookingAmount.toLocaleString() + ' ' + Pluralize(estimatedBookingAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)));
Grocy.Components.DateTimePicker.GetInputElement().attr('required', '');
}
else if (newAmount < productStockAmount + containerWeight)
{
$('#inventory-change-info').text(L('This means #1 will be removed from stock', estimatedBookingAmount.toLocaleString() + ' ' + Pluralize(estimatedBookingAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)));
Grocy.Components.DateTimePicker.GetInputElement().removeAttr('required');
}
Grocy.FrontendHelpers.ValidateForm('inventory-form');
},

View File

@@ -169,6 +169,8 @@ $('.input-group-qu').on('change', function(e)
$('#qu-conversion-info').addClass('d-none');
}
$("#tare_weight_qu_info").text($("#qu_id_stock option:selected").text());
Grocy.FrontendHelpers.ValidateForm('product-form');
});
@@ -199,6 +201,20 @@ $('#product-form input').keydown(function(event)
}
});
$("#enable_tare_weight_handling").on("click", function()
{
if (this.checked)
{
$("#tare_weight").removeAttr("disabled");
}
else
{
$("#tare_weight").attr("disabled", "");
}
Grocy.FrontendHelpers.ValidateForm("product-form");
});
Grocy.DeleteProductPictureOnSave = false;
$('#delete-current-product-picture-button').on('click', function (e)
{

View File

@@ -48,7 +48,7 @@
);
}
var successMessage = L('Added #1 #2 of #3 to stock', amount, Pluralize(amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + result.booking_id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>';
var successMessage = L('Added #1 #2 of #3 to stock', result.amount, Pluralize(result.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + result.id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>';
if (addBarcode !== undefined)
{
@@ -64,8 +64,13 @@
{
Grocy.FrontendHelpers.EndUiBusy("purchase-form");
toastr.success(successMessage);
$("#amount").attr("min", "1");
$("#amount").attr("step", "1");
$("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '1'));
$('#amount').val(0);
$('#price').val('');
$("#tare-weight-handling-info").addClass("d-none");
Grocy.Components.LocationPicker.Clear();
Grocy.Components.DateTimePicker.Clear();
Grocy.Components.ProductPicker.SetValue('');
@@ -97,7 +102,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
Grocy.Components.ProductCard.Refresh(productId);
Grocy.Api.Get('stock/products/' + productId,
function(productDetails)
function (productDetails)
{
$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name);
$('#price').val(productDetails.last_price);
@@ -116,6 +121,18 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
$("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '1'));
}
if (productDetails.product.enable_tare_weight_handling == 1)
{
var minAmount = parseFloat(productDetails.product.tare_weight) + parseFloat(productDetails.stock_amount) + 1;
$("#amount").attr("min", minAmount);
$("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', minAmount.toLocaleString()));
$("#tare-weight-handling-info").removeClass("d-none");
}
else
{
$("#tare-weight-handling-info").addClass("d-none");
}
if (productDetails.product.default_best_before_days.toString() !== '0')
{
if (productDetails.product.default_best_before_days == -1)

View File

@@ -63,7 +63,7 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->LocalizeForSqlString('Salami')}', 2, 3, 3, 1, 6); --18
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->LocalizeForSqlString('Toast')}', 4, 5, 5, 1, 2); --19
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->LocalizeForSqlString('Minced meat')}', 2, 3, 3, 1, 4); --20
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->LocalizeForSqlString('Flour')}', 2, 3, 3, 1, 3); --21
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, enable_tare_weight_handling, tare_weight) VALUES ('{$localizationService->LocalizeForSqlString('Flour')}', 3, 8, 8, 1, 3, 1, 500); --21
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->LocalizeForSqlString('Sugar')}', 3, 3, 3, 1, 3); --22
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->LocalizeForSqlString('Milk')}', 2, 10, 10, 1); --23
@@ -191,15 +191,15 @@ class DemoDataGeneratorService extends BaseService
$stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice());
$stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice());
$stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice());
$stockService->AddProduct(21, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice());
$stockService->AddProduct(21, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice());
$stockService->AddProduct(21, 1500, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice());
$stockService->AddProduct(21, 2500, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice());
$stockService->AddProduct(22, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice());
$stockService->AddProduct(22, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice());
$stockService->AddProduct(23, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice());
$stockService->AddProduct(23, 1, date('Y-m-d', strtotime('+2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice());
$stockService->AddMissingProductsToShoppingList();
$stockService->OpenProduct(3, 1);
$stockService->OpenProduct(6, 1);
$stockService->OpenProduct(21, 1);
$stockService->OpenProduct(22, 1);
$choresService = new ChoresService();

View File

@@ -130,6 +130,20 @@ class StockService extends BaseService
throw new \Exception('Product does not exist');
}
// Tare weight handling
// The given amount is the new total amount including the container weight (gross)
// The amount to be posted needs to be the given amount - stock amount - tare weight
$productDetails = (object)$this->GetProductDetails($productId);
if ($productDetails->product->enable_tare_weight_handling == 1)
{
if ($amount <= $productDetails->product->tare_weight + $productDetails->stock_amount)
{
throw new \Exception('The amount cannot be lower or equal than the defined tare weight + current stock amount');
}
$amount = $amount - $productDetails->stock_amount - $productDetails->product->tare_weight;
}
if ($transactionType === self::TRANSACTION_TYPE_PURCHASE || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION)
{
$stockId = uniqid();
@@ -174,6 +188,20 @@ class StockService extends BaseService
throw new \Exception('Product does not exist');
}
// Tare weight handling
// The given amount is the new total amount including the container weight (gross)
// The amount to be posted needs to be the absolute value of the given amount - stock amount - tare weight
$productDetails = (object)$this->GetProductDetails($productId);
if ($productDetails->product->enable_tare_weight_handling == 1)
{
if ($amount < $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);
}
if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION)
{
$productStockAmount = $this->Database->stock()->where('product_id', $productId)->sum('amount');
@@ -259,18 +287,36 @@ class StockService extends BaseService
throw new \Exception('Product does not exist');
}
$productStockAmount = $this->Database->stock()->where('product_id', $productId)->sum('amount');
$productDetails = (object)$this->GetProductDetails($productId);
if ($newAmount > $productStockAmount)
// Tare weight handling
// The given amount is the new total amount including the container weight (gross)
// So assume that the amount in stock is the amount also including the container weight
$containerWeight = 0;
if ($productDetails->product->enable_tare_weight_handling == 1)
{
$productDetails = $this->GetProductDetails($productId);
$amountToAdd = $newAmount - $productStockAmount;
$this->AddProduct($productId, $amountToAdd, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION, date('Y-m-d'), $productDetails['last_price']);
$containerWeight = $productDetails->product->tare_weight;
}
else if ($newAmount < $productStockAmount)
if ($newAmount > $productDetails->stock_amount + $containerWeight)
{
$amountToRemove = $productStockAmount - $newAmount;
$this->ConsumeProduct($productId, $amountToRemove, false, self::TRANSACTION_TYPE_INVENTORY_CORRECTION);
$bookingAmount = $newAmount - $productDetails->stock_amount;
if ($productDetails->product->enable_tare_weight_handling == 1)
{
$bookingAmount = $newAmount;
}
$this->AddProduct($productId, $bookingAmount, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION, date('Y-m-d'), $productDetails->last_price);
}
else if ($newAmount < $productDetails->stock_amount + $containerWeight)
{
$bookingAmount = $productDetails->stock_amount - $newAmount;
if ($productDetails->product->enable_tare_weight_handling == 1)
{
$bookingAmount = $newAmount;
}
$this->ConsumeProduct($productId, $bookingAmount, false, self::TRANSACTION_TYPE_INVENTORY_CORRECTION);
}
return $this->Database->lastInsertId();

View File

@@ -12,10 +12,11 @@
@php if(empty($additionalGroupCssClasses)) { $additionalGroupCssClasses = ''; } @endphp
@php if(empty($additionalAttributes)) { $additionalAttributes = ''; } @endphp
@php if(empty($additionalHtmlElements)) { $additionalHtmlElements = ''; } @endphp
@php if(empty($additionalHtmlContextHelp)) { $additionalHtmlContextHelp = ''; } @endphp
@php if(!isset($isRequired)) { $isRequired = true; } @endphp
<div class="form-group {{ $additionalGroupCssClasses }}">
<label for="{{ $id }}">{{ $L($label) }}&nbsp;&nbsp;<span id="{{ $hintId }}" class="small text-muted">{{ $hint }}</span></label>
<label for="{{ $id }}">{{ $L($label) }}&nbsp;&nbsp;<span id="{{ $hintId }}" class="small text-muted">{{ $hint }}</span>{!! $additionalHtmlContextHelp !!}</label>
<div class="input-group">
<input {!! $additionalAttributes !!} type="number" class="form-control numberpicker {{ $additionalCssClasses }}" id="{{ $id }}" name="{{ $id }}" value="{{ $value }}" min="{{ $min }}" max="{{ $max }}" step="{{ $step }}" @if($isRequired) required @endif>
<div class="input-group-append">

View File

@@ -23,7 +23,8 @@
'hintId' => 'amount_qu_unit',
'min' => 1,
'value' => 1,
'invalidFeedback' => $L('The amount cannot be lower than #1', '1')
'invalidFeedback' => $L('The amount cannot be lower than #1', '1'),
'additionalHtmlContextHelp' => '<div id="tare-weight-handling-info" class="text-info font-italic d-none">' . $L('Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated') . '</div>'
))
<div class="form-group">

View File

@@ -22,9 +22,10 @@
'hintId' => 'new_amount_qu_unit',
'min' => 0,
'value' => 1,
'invalidFeedback' => $L('The amount cannot be lower than #1', '1'),
'invalidFeedback' => $L('The amount cannot be lower than #1', '0'),
'additionalAttributes' => 'data-notequal="notequal" not-equal="-1"',
'additionalHtmlElements' => '<div id="inventory-change-info" class="form-text text-muted small d-none"></div>'
'additionalHtmlElements' => '<div id="inventory-change-info" class="form-text text-muted small d-none"></div>',
'additionalHtmlContextHelp' => '<div id="tare-weight-handling-info" class="text-small text-info font-italic d-none">' . $L('Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated') . '</div>'
))
@include('components.datetimepicker', array(

View File

@@ -134,6 +134,29 @@
</div>
</div>
<div class="form-group">
<div class="form-check">
<input type="hidden" name="enable_tare_weight_handling" value="0">
<input @if($mode == 'edit' && $product->enable_tare_weight_handling == 1) checked @endif class="form-check-input" type="checkbox" id="enable_tare_weight_handling" name="enable_tare_weight_handling" value="1">
<label class="form-check-label" for="enable_tare_weight_handling">{{ $L('Enable tare weight handling') }}
<span class="text-muted small">{{ $L('This is useful e.g. for flour in jars - on purchase/consume/inventory you always weigh the whole jar, the amount to be posted is then automatically calculated based on what is in stock and the tare weight defined below') }}</span>
</label>
</div>
</div>
@php if($mode == 'edit') { $value = $product->tare_weight; } else { $value = 0; } @endphp
@php if(($mode == 'edit' && $product->enable_tare_weight_handling == 0) || $mode == 'create') { $additionalAttributes = 'disabled'; } else { $additionalAttributes = ''; } @endphp
@include('components.numberpicker', array(
'id' => 'tare_weight',
'label' => 'Tare weight',
'min' => 0,
'step' => 0.01,
'value' => $value,
'invalidFeedback' => $L('This cannot be lower than #1', '0'),
'additionalAttributes' => $additionalAttributes,
'hintId' => 'tare_weight_qu_info'
))
<div class="form-group">
<label for="product-picture">{{ $L('Product picture') }}
<span class="text-muted small">{{ $L('If you don\'t select a file, the current picture will not be altered') }}</span>

View File

@@ -35,7 +35,8 @@
'label' => 'Amount',
'hintId' => 'amount_qu_unit',
'min' => 1,
'invalidFeedback' => $L('The amount cannot be lower than #1', '1')
'invalidFeedback' => $L('The amount cannot be lower than #1', '1'),
'additionalHtmlContextHelp' => '<div id="tare-weight-handling-info" class="text-info font-italic d-none">' . $L('Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated') . '</div>'
))
@include('components.numberpicker', array(

View File

@@ -88,7 +88,7 @@
<span class="small text-muted">{{ $L('Based on the prices of the last purchase per product') }}</span>
</label>
<p class="font-weight-bold font-italic">
<span class="local-number-format" data-format="currency">{{ $totalRecipeCosts }}</span> {{ GROCY_CURRENCY }}
<span class="locale-number-format" data-format="currency">{{ $totalRecipeCosts }}</span> {{ GROCY_CURRENCY }}
</p>
</div>
</div>