product barcodes with QU and Stores

This commit is contained in:
Kurt Riddlesperger 2020-04-24 00:19:21 -05:00
parent 23d80c3b77
commit 6676e3c508
14 changed files with 559 additions and 9 deletions

View File

@ -23,6 +23,18 @@ class StockApiController extends BaseApiController
}
}
public function ProductBarcodeDetails(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
try
{
return $this->ApiResponse($response, $this->getDatabase()->product_barcodes()->where('barcode = :1', $args['barcode'])->fetch());
}
catch (\Exception $ex)
{
return $this->GenericErrorResponse($response, $ex->getMessage());
}
}
public function ProductDetailsByBarcode(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
try

View File

@ -179,6 +179,7 @@ class StockController extends BaseController
{
return $this->renderPage($response, 'productform', [
'locations' => $this->getDatabase()->locations()->orderBy('name'),
'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'),
'productgroups' => $this->getDatabase()->product_groups()->orderBy('name'),
@ -195,6 +196,7 @@ class StockController extends BaseController
return $this->renderPage($response, 'productform', [
'product' => $product,
'locations' => $this->getDatabase()->locations()->orderBy('name'),
'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'),
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name'),
'productgroups' => $this->getDatabase()->product_groups()->orderBy('name'),
@ -354,6 +356,34 @@ class StockController extends BaseController
]);
}
public function ProductBarcodesEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
$product = null;
if (isset($request->getQueryParams()['product']))
{
$product = $this->getDatabase()->products($request->getQueryParams()['product']);
}
if ($args['productBarcodeId'] == 'new')
{
return $this->renderPage($response, 'productbarcodesform', [
'mode' => 'create',
'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'),
'product' => $product,
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name')
]);
}
else
{
return $this->renderPage($response, 'productbarcodesform', [
'mode' => 'edit',
'barcode' => $this->getDatabase()->product_barcodes($args['productBarcodeId']),
'product' => $product,
'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name')
]);
}
}
public function QuantityUnitConversionEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
$product = null;

View File

@ -2703,6 +2703,57 @@
}
}
},
"/productbarcodedetails/{barcode}": {
"get": {
"summary": "Executes a product barcode details lookoup via the configured plugin with the given barcode",
"tags": [
"Product"
],
"parameters": [
{
"in": "path",
"name": "barcode",
"required": true,
"description": "The barcode to lookup up",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "add",
"required": false,
"description": "When true, the product is added to the database on a successful lookup and the new product id is in included in the response",
"schema": {
"type": "boolean",
"default": false
}
}
],
"responses": {
"200": {
"description": "An ProductBarcodeDetails object or null, when nothing was found for the given barcode",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductBarcodeDetailsResponse"
}
}
}
},
"400": {
"description": "The operation was not successful (possible errors are: Plugin error)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GenericErrorResponse"
}
}
}
}
}
}
},
"/recipes/{recipeId}/add-not-fulfilled-products-to-shoppinglist": {
"post": {
"summary": "Adds all missing products for the given recipe to the shopping list",
@ -3374,6 +3425,7 @@
"enum": [
"products",
"chores",
"product_barcodes",
"batteries",
"locations",
"quantity_units",
@ -3862,6 +3914,28 @@
}
}
},
"ProductBarcodeDetailsResponse": {
"type": "object",
"properties": {
"product_id": {
"type": "integer"
},
"barcode": {
"type": "string"
},
"qu_factor_purchase_to_stock": {
"type": "number",
"format": "number"
},
"barcode": {
"type": "string",
"description": "Can contain multiple barcodes separated by comma"
},
"shopping_location_id": {
"type": "integer"
}
}
},
"ExternalBarcodeLookupResponse": {
"type": "object",
"properties": {

19
migrations/0103.sql Normal file
View File

@ -0,0 +1,19 @@
ALTER TABLE stock_log
ADD qu_factor_purchase_to_stock REAL NOT NULL DEFAULT 1.0;
ALTER TABLE stock
ADD qu_factor_purchase_to_stock REAL NOT NULL DEFAULT 1.0;
UPDATE stock
SET qu_factor_purchase_to_stock = (select qu_factor_purchase_to_stock from products where product_id = id);
UPDATE stock_log
SET qu_factor_purchase_to_stock = (select qu_factor_purchase_to_stock from products where product_id = id);
CREATE TABLE product_barcodes (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
product_id INT NOT NULL,
barcode TEXT NOT NULL,
qu_factor_purchase_to_stock REAL NOT NULL DEFAULT 1,
shopping_location_id INTEGER
);

View File

@ -80,7 +80,14 @@ Grocy.Components.ProductCard.Refresh = function(productId)
if (productDetails.last_price !== null)
{
$('#productcard-product-last-price').text(Number.parseFloat(productDetails.last_price).toLocaleString() + ' ' + Grocy.Currency + ' per ' + productDetails.quantity_unit_purchase.name);
if (productDetails.last_qu_factor_purchase_to_stock > 1)
{
$('#productcard-product-last-price').text(Number.parseFloat(productDetails.last_price).toLocaleString() + ' ' + Grocy.Currency + ' per 1 ' + productDetails.quantity_unit_purchase.name + ' of ' + productDetails.last_qu_factor_purchase_to_stock + ' ' + productDetails.quantity_unit_stock.name_plural);
}
else
{
$('#productcard-product-last-price').text(Number.parseFloat(productDetails.last_price).toLocaleString() + ' ' + Grocy.Currency + ' per ' + productDetails.quantity_unit_purchase.name);
}
}
else
{

View File

@ -132,6 +132,7 @@ $('#product_id_text_input').on('blur', function(e)
{
return;
}
$('#product_id').attr("barcode", "null");
var input = $('#product_id_text_input').val().toString();
var possibleOptionElement = $("#product_id option[data-additional-searchdata*=\"" + input + ",\"]").first();
@ -139,6 +140,7 @@ $('#product_id_text_input').on('blur', function(e)
if (GetUriParam('addbarcodetoselection') === undefined && input.length > 0 && possibleOptionElement.length > 0)
{
$('#product_id').val(possibleOptionElement.val());
$('#product_id').attr("barcode", input);
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
}

View File

@ -180,6 +180,11 @@ var calendar = $("#calendar").fullCalendar({
productDetails.last_price = 0;
}
if (productDetails.last_qu_factor_purchase_to_stock === null)
{
productDetails.last_qu_factor_purchase_to_stock = 1;
}
element.attr("data-product-details", event.productDetails);
var productOrderMissingButtonDisabledClasses = "disabled";
@ -205,7 +210,7 @@ var calendar = $("#calendar").fullCalendar({
var costsAndCaloriesPerServing = ""
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{
costsAndCaloriesPerServing = '<h5 class="small text-truncate"><span class="locale-number locale-number-currency">' + productDetails.last_price / productDetails.product.qu_factor_purchase_to_stock * mealPlanEntry.product_amount + '</span> / <span class="locale-number locale-number-generic">' + productDetails.product.calories * mealPlanEntry.product_amount + '</span> kcal ' + '<h5>';
costsAndCaloriesPerServing = '<h5 class="small text-truncate"><span class="locale-number locale-number-currency">' + productDetails.last_price / productDetails.last_qu_factor_purchase_to_stock * mealPlanEntry.product_amount + '</span> / <span class="locale-number locale-number-generic">' + productDetails.product.calories * mealPlanEntry.product_amount + '</span> kcal ' + '<h5>';
}
else
{

View File

@ -0,0 +1,98 @@
$('#save-barcode-button').on('click', function(e)
{
e.preventDefault();
var jsonData = $('#barcode-form').serializeJSON();
Grocy.FrontendHelpers.BeginUiBusy("barcode-form");
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('objects/product_barcodes', jsonData,
function(result)
{
Grocy.EditObjectId = result.created_object_id;
window.location.href = U("/product/" + GetUriParam("product"));
},
function(xhr)
{
Grocy.FrontendHelpers.EndUiBusy("barcode-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
}
);
}
else
{
Grocy.Api.Put('objects/product_barcodes/' + Grocy.EditObjectId, jsonData,
function(result)
{
window.location.href = U("/product/" + GetUriParam("product"));
},
function(xhr)
{
Grocy.FrontendHelpers.EndUiBusy("barcode-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
}
);
}
Grocy.Api.Get('stock/products/' + jsonData.product_id,
function(productDetails)
{
var existingBarcodes = productDetails.product.barcode || '';
if (existingBarcodes.length === 0)
{
productDetails.product.barcode = jsonData.barcode;
}
else
{
productDetails.product.barcode += ',' + jsonData.barcode;
}
var jsonDataProduct = {};
jsonDataProduct.barcode = productDetails.product.barcode;
Grocy.Api.Put('objects/products/' + jsonData.product_id, jsonDataProduct,
function(result)
{
},
function(xhr)
{
Grocy.FrontendHelpers.EndUiBusy("barcode-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
},
function(xhr)
{
Grocy.FrontendHelpers.EndUiBusy("barcode-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
}
);
});
$('#barcode').on('change', function(e)
{
Grocy.FrontendHelpers.ValidateForm('barcode-form');
});
$('#qu_factor_purchase_to_stock').on('change', function(e)
{
Grocy.FrontendHelpers.ValidateForm('barcode-form');
});
$('#barcode-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('barcode-form').checkValidity() === false) //There is at least one validation error
{
return false;
}
else
{
$('#save-barcode-button').click();
}
}
});
Grocy.FrontendHelpers.ValidateForm('barcode-form');

View File

@ -249,12 +249,28 @@ $('#product-form input').keyup(function(event)
{
$("#qu-conversion-add-button").removeClass("disabled");
}
if (document.getElementById('product-form').checkValidity() === false) //There is at least one validation error
{
$("#barcode-add-button").addClass("disabled");
}
else
{
$("#barcode-add-button").removeClass("disabled");
}
});
$('#product-form select').change(function(event)
{
Grocy.FrontendHelpers.ValidateForm('product-form');
if (document.getElementById('product-form').checkValidity() === false) //There is at least one validation error
{
$("#barcode-add-button").addClass("disabled");
}
else
{
$("#barcode-add-button").removeClass("disabled");
}
if (document.getElementById('product-form').checkValidity() === false) //There is at least one validation error
{
$("#qu-conversion-add-button").addClass("disabled");
@ -365,6 +381,18 @@ var quConversionsTable = $('#qu-conversions-table').DataTable({
$('#qu-conversions-table tbody').removeClass("d-none");
quConversionsTable.columns.adjust().draw();
var barcodeTable = $('#barcode-table').DataTable({
'order': [[1, 'asc']],
"orderFixed": [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
});
$('#barcode-table tbody').removeClass("d-none");
barcodeTable.columns.adjust().draw();
Grocy.Components.UserfieldsForm.Load();
$("#name").trigger("keyup");
$('#name').focus();
@ -407,6 +435,29 @@ $(document).on('click', '.qu-conversion-delete-button', function(e)
console.error(xhr);
}
);
var newBarcode = '';
productBarcode.split(',').forEach(function(item)
{
if(barcode != item)
{
newBarcode += ',' + item;
}
});
var jsonDataProduct = {};
jsonDataProduct.barcode = newBarcode;
Grocy.Api.Put('objects/products/' + productId, jsonDataProduct,
function(result)
{
},
function(xhr)
{
Grocy.FrontendHelpers.EndUiBusy("product-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
}
});
@ -425,6 +476,82 @@ $("#qu-conversion-add-button").on("click", function(e)
$('#save-product-button').click();
});
$(document).on('click', '.barcode-delete-button', function(e)
{
var objectId = $(e.currentTarget).attr('data-barcode-id');
var productId = $(e.currentTarget).attr('data-product-id');
var barcode = $(e.currentTarget).attr('data-barcode');
var productBarcode = $(e.currentTarget).attr('data-product-barcode');
bootbox.confirm({
message: __t('Are you sure to remove this barcode?'),
closeButton: false,
buttons: {
confirm: {
label: __t('Yes'),
className: 'btn-success'
},
cancel: {
label: __t('No'),
className: 'btn-danger'
}
},
callback: function(result)
{
if (result === true)
{
Grocy.Api.Delete('objects/product_barcodes/' + objectId, { },
function(result)
{
Grocy.ProductEditFormRedirectUri = "reload";
$('#save-product-button').click();
},
function(xhr)
{
console.error(xhr);
}
);
var newBarcode = '';
productBarcode.split(',').forEach(function(item)
{
if(barcode != item)
{
newBarcode += ',' + item;
}
});
var jsonDataProduct = {};
jsonDataProduct.barcode = newBarcode;
Grocy.Api.Put('objects/products/' + productId, jsonDataProduct,
function(result)
{
},
function(xhr)
{
Grocy.FrontendHelpers.EndUiBusy("product-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
}
});
});
$(document).on('click', '.barcode-edit-button', function (e)
{
var id = $(e.currentTarget).attr('data-barcode-id');
Grocy.ProductEditFormRedirectUri = U("/productbarcodes/" + id.toString() + "?product=editobjectid");
$('#save-product-button').click();
});
$("#barcode-add-button").on("click", function(e)
{
Grocy.ProductEditFormRedirectUri = U("/productbarcodes/new?product=editobjectid");
$('#save-product-button').click();
});
$('#qu_id_purchase').blur(function(e)
{
// Preset the stock quantity unit with the purchase quantity unit, if the stock quantity unit is unset.

View File

@ -141,13 +141,66 @@ if (Grocy.Components.ProductPicker !== undefined)
{
Grocy.Components.ProductCard.Refresh(productId);
if (document.getElementById("product_id").getAttribute("barcode") != "null")
{
Grocy.Api.Get('productbarcodedetails/' + document.getElementById("product_id").getAttribute("barcode"),
function(resultBarcode)
{
if (resultBarcode != null)
{
$('#product_id').attr("barcode-qu-factor-purchase-to-stock", resultBarcode.qu_factor_purchase_to_stock);
$('#product_id').attr("barcode-shopping-location-id", resultBarcode.shopping_location_id);
}
else
{
$('#product_id').attr("barcode-qu-factor-purchase-to-stock", "null");
$('#product_id').attr("barcode-shopping-location-id", "null");
}
},
function(xhr)
{
console.error(xhr);
}
);
}
else
{
$('#product_id').attr("barcode-qu-factor-purchase-to-stock", "null");
$('#product_id').attr("barcode-shopping-location-id", "null");
}
Grocy.Api.Get('stock/products/' + productId,
function(productDetails)
{
$('#price').val(parseFloat(productDetails.last_price).toLocaleString({ minimumFractionDigits: 2, maximumFractionDigits: 2 }));
$('#price').val(parseFloat(productDetails.last_price).toLocaleString({ minimumFractionDigits: 2, maximumFractionDigits: 2 }));
var qu_factor_purchase_to_stock = null;
var barcode_shopping_location_id = null;
if (document.getElementById("product_id").getAttribute("barcode") != "null" && document.getElementById("product_id").getAttribute("barcode-qu-factor-purchase-to-stock") != "null")
{
qu_factor_purchase_to_stock = document.getElementById("product_id").getAttribute("barcode-qu-factor-purchase-to-stock");
barcode_shopping_location_id = document.getElementById("product_id").getAttribute("barcode-shopping-location-id");
}
else
{
if (productDetails.last_qu_factor_purchase_to_stock != null)
{
qu_factor_purchase_to_stock = productDetails.last_qu_factor_purchase_to_stock;
}
else
{
qu_factor_purchase_to_stock = productDetails.product.qu_factor_purchase_to_stock;
}
}
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) {
if (productDetails.last_shopping_location_id != null)
if (barcode_shopping_location_id != null)
{
Grocy.Components.ShoppingLocationPicker.SetId(barcode_shopping_location_id);
}
else if (productDetails.last_shopping_location_id != null)
{
Grocy.Components.ShoppingLocationPicker.SetId(productDetails.last_shopping_location_id);
}
@ -162,20 +215,21 @@ if (Grocy.Components.ProductPicker !== undefined)
Grocy.Components.LocationPicker.SetId(productDetails.location.id);
}
$('#amount_qu_unit').attr("qu-factor-purchase-to-stock", productDetails.product.qu_factor_purchase_to_stock);
$('#amount_qu_unit').attr("qu-factor-purchase-to-stock", qu_factor_purchase_to_stock);
$('#amount_qu_unit').attr("quantity-unit-purchase-name", productDetails.quantity_unit_purchase.name);
$('#amount_qu_unit').attr("quantity-unit-stock-name", productDetails.quantity_unit_stock.name);
$('#amount_qu_unit').attr("quantity-unit-stock-name-plural", productDetails.quantity_unit_stock.name_plural);
$('#qu_factor_purchase_to_stock').val(productDetails.product.qu_factor_purchase_to_stock);
$('#qu_factor_purchase_to_stock').val(qu_factor_purchase_to_stock);
if (productDetails.product.qu_id_purchase === productDetails.product.qu_id_stock)
if (qu_factor_purchase_to_stock == 1)
{
$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name);
$('#group-qu_factor_purchase_to_stock').addClass('d-none');
}
else
{
$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name + " (" + __t("will be multiplied by a factor of %1$s to get %2$s", parseFloat(productDetails.product.qu_factor_purchase_to_stock).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: 2 }), __n(2, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)) + ")");
$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name + " (" + __t("will be multiplied by a factor of %1$s to get %2$s", parseFloat(qu_factor_purchase_to_stock).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: 2 }), __n(2, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)) + ")");
$('#group-qu_factor_purchase_to_stock').removeClass('d-none');
}
@ -200,7 +254,7 @@ if (Grocy.Components.ProductPicker !== undefined)
if (productDetails.product.enable_tare_weight_handling == 1)
{
var minAmount = parseFloat(productDetails.product.tare_weight) / productDetails.product.qu_factor_purchase_to_stock + parseFloat(productDetails.stock_amount);
var minAmount = parseFloat(productDetails.product.tare_weight) / qu_factor_purchase_to_stock + parseFloat(productDetails.stock_amount);
$("#amount").attr("min", minAmount);
$("#amount").attr("step", "0.0001");
$("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %s', minAmount.toLocaleString()));

View File

@ -45,6 +45,7 @@ $app->group('', function(RouteCollectorProxy $group)
$group->get('/stockentry/{entryId}', '\Grocy\Controllers\StockController:StockEntryEditForm');
$group->get('/products', '\Grocy\Controllers\StockController:ProductsList');
$group->get('/product/{productId}', '\Grocy\Controllers\StockController:ProductEditForm');
$group->get('/productbarcodes/{productBarcodeId}', '\Grocy\Controllers\StockController:ProductBarcodesEditForm');
$group->get('/stocksettings', '\Grocy\Controllers\StockController:StockSettings');
$group->get('/locations', '\Grocy\Controllers\StockController:LocationsList');
$group->get('/location/{locationId}', '\Grocy\Controllers\StockController:LocationEditForm');
@ -199,6 +200,7 @@ $app->group('/api', function(RouteCollectorProxy $group)
$group->get('/stock/transactions/{transactionId}', '\Grocy\Controllers\StockApiController:StockTransactions');
$group->post('/stock/transactions/{transactionId}/undo', '\Grocy\Controllers\StockApiController:UndoTransaction');
$group->get('/stock/barcodes/external-lookup/{barcode}', '\Grocy\Controllers\StockApiController:ExternalBarcodeLookup');
$group->get('/productbarcodedetails/{barcode}', '\Grocy\Controllers\StockApiController:ProductBarcodeDetails');
}
// Shopping list

View File

@ -127,12 +127,14 @@ class StockService extends BaseService
$averageShelfLifeDays = intval($this->getDatabase()->stock_average_product_shelf_life()->where('id', $productId)->fetch()->average_shelf_life_days);
$lastPrice = null;
$lastQuFactorPurchaseToStock = null;
$defaultShoppingLocation = null;
$lastShoppingLocation = null;
$lastLogRow = $this->getDatabase()->stock_log()->where('product_id = :1 AND transaction_type IN (:2, :3) AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE, self::TRANSACTION_TYPE_INVENTORY_CORRECTION)->orderBy('row_created_timestamp', 'DESC')->limit(1)->fetch();
if ($lastLogRow !== null && !empty($lastLogRow))
{
$lastPrice = $lastLogRow->price;
$lastQuFactorPurchaseToStock = $lastLogRow->qu_factor_purchase_to_stock;
$lastShoppingLocation = $lastLogRow->shopping_location_id;
}
@ -155,6 +157,7 @@ class StockService extends BaseService
'quantity_unit_purchase' => $quPurchase,
'quantity_unit_stock' => $quStock,
'last_price' => $lastPrice,
'last_qu_factor_purchase_to_stock' => $lastQuFactorPurchaseToStock,
'last_shopping_location_id' => $lastShoppingLocation,
'default_shopping_location_id' => $product->shopping_location_id,
'next_best_before_date' => $nextBestBeforeDate,

View File

@ -0,0 +1,68 @@
@extends('layout.default')
@if($mode == 'edit')
@section('title', $__t('Edit Barcodes'))
@else
@section('title', $__t('Create Barcodes'))
@endif
@section('viewJsName', 'productbarcodesform')
@section('content')
<div class="row">
<div class="col">
<h2 class="title">@yield('title')</h2>
<hr>
</div>
</div>
<div class="row">
<div class="col-lg-6 col-xs-12">
<h3 class="text-muted">{{ $__t('Barcode for product') }} <strong>{{ $product->name }}</strong></h3>
<script>Grocy.EditMode = '{{ $mode }}';</script>
@if($mode == 'edit')
<script>Grocy.EditObjectId = {{ $barcode->id }};</script>
@endif
<form id="barcode-form" novalidate>
<input type="hidden" name="product_id" value="{{ $product->id }}">
<div class="form-group">
<label for="name">{{ $__t('Barcode') }}<i class="fas fa-barcode"></i></label>
<input type="text" class="form-control" required id="barcode" name="barcode" value="@if($mode == 'edit'){{ $barcode->barcode }}@endif">
</div>
@php if($mode == 'edit') { $value = $barcode->qu_factor_purchase_to_stock; } else { $value = 1; } @endphp
@include('components.numberpicker', array(
'id' => 'qu_factor_purchase_to_stock',
'label' => 'Factor purchase to stock quantity unit',
'min' => 1,
'value' => $value,
'isRequired' => true,
'invalidFeedback' => $__t('The amount cannot be lower than %s', '1'),
'additionalCssClasses' => 'input-group-qu',
))
@if(GROCY_FEATURE_FLAG_PRICE_TRACKING)
<div class="form-group">
<label for="shopping_location_id_id">{{ $__t('Default store') }}</label>
<select class="form-control" id="shopping_location_id" name="shopping_location_id">
<option></option>
@foreach($shoppinglocations as $store)
<option @if($mode == 'edit' && $store->id == $product->shopping_location_id) selected="selected" @endif value="{{ $store->id }}">{{ $store->name }}</option>
@endforeach
</select>
</div>
@else
<input type="hidden" name="shopping_location_id" id="shopping_location_id" value="1">
@endif
<button id="save-barcode-button" class="btn btn-success">{{ $__t('Save') }}</button>
</form>
</div>
</div>
@stop

View File

@ -346,6 +346,55 @@
</tbody>
</table>
<h2>
{{ $__t('Barcode Details') }}
<a id="barcode-add-button" class="btn btn-outline-dark" href="#">
<i class="fas fa-plus"></i> {{ $__t('Add') }}
</a>
</h2>
<h5 id="barcode-headline-info" class="text-muted font-italic"></h5>
<table id="barcode-table" class="table table-sm table-striped dt-responsive">
<thead>
<tr>
<th class="border-right"></th>
<th>{{ $__t('Barcode') }}</th>
<th>{{ $__t('QU Factor Purchase To Stock') }}</th>
<th>{{ $__t('Shopping Location') }}</th>
</tr>
</thead>
<tbody class="d-none">
@if($mode == "edit")
@foreach($barcodes as $barcode)
@if($barcode->product_id == $product->id || $barcode->product_id == null)
<tr>
<td class="fit-content border-right">
<a class="btn btn-sm btn-info barcode-edit-button @if($barcode->product_id == null) disabled @endif" href="#" data-barcode-id="{{ $barcode->id }}">
<i class="fas fa-edit"></i>
</a>
<a class="btn btn-sm btn-danger barcode-delete-button @if($barcode->product_id == null) disabled @endif" href="#" data-barcode-id="{{ $barcode->id }}" data-barcode="{{ $barcode->barcode }}" data-product-barcode="{{ $product->barcode }}" data-product-id="{{ $product->id }}">
<i class="fas fa-trash"></i>
</a>
</td>
<td>
{{ $barcode->barcode }}
</td>
<td>
<span class="locale-number locale-number-quantity-amount">{{ $barcode->qu_factor_purchase_to_stock }}</span>
</td>
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<td id="barcode-shopping-location">
@if (FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $barcode->shopping_location_id) !== null)
{{ FindObjectInArrayByPropertyValue($shoppinglocations, 'id', $barcode->shopping_location_id)->name }}
@endif
</td>
@endif
</tr>
@endif
@endforeach
@endif
</tbody>
</table>
<div class="pt-5">
<label class="mt-2">{{ $__t('Picture') }}</label>
<button id="delete-current-product-picture-button" class="btn btn-sm btn-danger @if(empty($product->picture_file_name)) disabled @endif"><i class="fas fa-trash"></i> {{ $__t('Delete') }}</button>