Added grocycode for recipes (closes #1562)

This commit is contained in:
Bernd Bestel 2022-02-11 17:49:30 +01:00
parent 51fdefaede
commit 222c518a5f
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
16 changed files with 212 additions and 16 deletions

View File

@ -17,6 +17,7 @@
- Background: Before v3.0.0 recipe costs were only based on the last price per product and since v3.0.0 the "real costs" (based on the default consume rule "Opened first, then first due first, then first in first out") are used, means out of stock items have no price - so using the last price for out of stock items should reflect the current real costs better - Background: Before v3.0.0 recipe costs were only based on the last price per product and since v3.0.0 the "real costs" (based on the default consume rule "Opened first, then first due first, then first in first out") are used, means out of stock items have no price - so using the last price for out of stock items should reflect the current real costs better
- Added a new recipes setting (top right corner settings menu) "Show the recipe list and the recipe side by side" (defaults to enabled, so no changed behaviour when not configured) - Added a new recipes setting (top right corner settings menu) "Show the recipe list and the recipe side by side" (defaults to enabled, so no changed behaviour when not configured)
- When disabled, on the recipes page, the recipe list is displayed full-width and the recipe will be shown in a popup instead of on the right side - When disabled, on the recipes page, the recipe list is displayed full-width and the recipe will be shown in a popup instead of on the right side
- Recipes are now also grocycode enabled (works like any other grocycode; download/print it via the recipes edit page or the more/context menu on the recipes page; use/scan it at any place a recipe can be selected)
- Performance improvements (page loading time) of the recipes page - Performance improvements (page loading time) of the recipes page
- Fixed that when adding missing recipe ingredients, with the option "Only check if any amount is in stock" enabled, to the shopping list, unit conversions (if any) weren't considered - Fixed that when adding missing recipe ingredients, with the option "Only check if any amount is in stock" enabled, to the shopping list, unit conversions (if any) weren't considered
- Fixed that the recipe stock fulfillment information about shopping list amounts was not correct when the ingredient had a decimal amount - Fixed that the recipe stock fulfillment information about shopping list amounts was not correct when the ingredient had a decimal amount
@ -73,4 +74,5 @@
- The API endpoint `/stock/shoppinglist/clear` has now a new optional request body parameter `done_only` (to only remove done items from the given shopping list, defaults to `false`) - The API endpoint `/stock/shoppinglist/clear` has now a new optional request body parameter `done_only` (to only remove done items from the given shopping list, defaults to `false`)
- The API endpoint `/chores/{choreId}/execute` has now a new optional request body parameter `skipped` (to skip the next chore schedule, defaults to `false`) - The API endpoint `/chores/{choreId}/execute` has now a new optional request body parameter `skipped` (to skip the next chore schedule, defaults to `false`)
- The API endpoint `/chores/{choreId}` has new response field/property `average_execution_frequency_hours` (contains the average past execution frequency in hours or `null`, when the chore was never executed before) - The API endpoint `/chores/{choreId}` has new response field/property `average_execution_frequency_hours` (contains the average past execution frequency in hours or `null`, when the chore was never executed before)
- New API endpoint `/recipes/{recipeId}/printlabel` (to print recipe grocycodes on the configured label printer)
- Fixed that the barcode lookup for the "Stock by-barcode" API endpoints was case sensitive - Fixed that the barcode lookup for the "Stock by-barcode" API endpoints was case sensitive

View File

@ -3,6 +3,8 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Controllers\Users\User; use Grocy\Controllers\Users\User;
use Grocy\Helpers\WebhookRunner;
use Grocy\Helpers\Grocycode;
class RecipesApiController extends BaseApiController class RecipesApiController extends BaseApiController
{ {
@ -76,4 +78,28 @@ class RecipesApiController extends BaseApiController
return $this->GenericErrorResponse($response, $ex->getMessage()); return $this->GenericErrorResponse($response, $ex->getMessage());
} }
} }
public function RecipePrintLabel(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
try
{
$recipe = $this->getDatabase()->recipes()->where('id', $args['recipeId'])->fetch();
$webhookData = array_merge([
'recipe' => $recipe->name,
'grocycode' => (string)(new Grocycode(Grocycode::RECIPE, $args['recipeId'])),
], GROCY_LABEL_PRINTER_PARAMS);
if (GROCY_LABEL_PRINTER_RUN_SERVER)
{
(new WebhookRunner())->run(GROCY_LABEL_PRINTER_WEBHOOK, $webhookData, GROCY_LABEL_PRINTER_HOOK_JSON);
}
return $this->ApiResponse($response, $webhookData);
}
catch (\Exception $ex)
{
return $this->GenericErrorResponse($response, $ex->getMessage());
}
}
} }

View File

@ -3,9 +3,12 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Services\RecipesService; use Grocy\Services\RecipesService;
use Grocy\Helpers\Grocycode;
class RecipesController extends BaseController class RecipesController extends BaseController
{ {
use GrocycodeTrait;
public function MealPlan(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function MealPlan(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
$start = date('Y-m-d'); $start = date('Y-m-d');
@ -213,4 +216,10 @@ class RecipesController extends BaseController
'mealplanSections' => $this->getDatabase()->meal_plan_sections()->where('id > 0')->orderBy('sort_number') 'mealplanSections' => $this->getDatabase()->meal_plan_sections()->where('id > 0')->orderBy('sort_number')
]); ]);
} }
public function RecipeGrocycodeImage(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
$gc = new Grocycode(Grocycode::RECIPE, $args['recipeId']);
return $this->ServeGrocycodeImage($request, $response, $gc);
}
} }

View File

@ -3496,6 +3496,48 @@
} }
} }
}, },
"/recipes/{recipeId}/printlabel": {
"get": {
"summary": "Prints the grocycode label of the given recipe on the configured label printer",
"tags": [
"Recipes"
],
"parameters": [
{
"in": "path",
"name": "recipeId",
"required": true,
"description": "A valid recipe id",
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "The operation was successful",
"content": {
"application/json": {
"schema": {
"type": "object",
"description": "WebHook data"
}
}
}
},
"400": {
"description": "The operation was not successful (possible errors are: Not existing recipe, error on WebHook execution)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error400"
}
}
}
}
}
}
},
"/chores": { "/chores": {
"get": { "get": {
"summary": "Returns all chores incl. the next estimated execution time per chore", "summary": "Returns all chores incl. the next estimated execution time per chore",

View File

@ -23,6 +23,8 @@ class Grocycode
public const CHORE = 'c'; public const CHORE = 'c';
public const RECIPE = 'r';
public const MAGIC = 'grcy'; public const MAGIC = 'grcy';
/** /**
@ -55,7 +57,7 @@ class Grocycode
/** /**
* An array that registers all valid grocycode types. Register yours here by appending to this array. * An array that registers all valid grocycode types. Register yours here by appending to this array.
*/ */
public static $Items = [self::PRODUCT, self::BATTERY, self::CHORE]; public static $Items = [self::PRODUCT, self::BATTERY, self::CHORE, self::RECIPE];
private $type; private $type;

View File

@ -37,7 +37,7 @@ Grocy.Components.RecipePicker.Clear = function()
$('.recipe-combobox').combobox({ $('.recipe-combobox').combobox({
appendId: '_text_input', appendId: '_text_input',
bsVersion: '4', bsVersion: '4',
clearIfNoMatch: true clearIfNoMatch: false
}); });
var prefillByName = Grocy.Components.RecipePicker.GetPicker().parent().data('prefill-by-name').toString(); var prefillByName = Grocy.Components.RecipePicker.GetPicker().parent().data('prefill-by-name').toString();
@ -66,3 +66,38 @@ if (typeof prefillById !== "undefined")
var nextInputElement = $(Grocy.Components.RecipePicker.GetPicker().parent().data('next-input-selector').toString()); var nextInputElement = $(Grocy.Components.RecipePicker.GetPicker().parent().data('next-input-selector').toString());
nextInputElement.focus(); nextInputElement.focus();
} }
$('#recipe_id_text_input').on('blur', function(e)
{
if ($('#recipe_id').hasClass("combobox-menu-visible"))
{
return;
}
var input = $('#recipe_id_text_input').val().toString();
var possibleOptionElement = [];
// grocycode handling
if (input.startsWith("grcy"))
{
var gc = input.split(":");
if (gc[1] == "r")
{
possibleOptionElement = $("#recipe_id option[value=\"" + gc[2] + "\"]").first();
}
if (possibleOptionElement.length > 0)
{
$('#recipe_id').val(possibleOptionElement.val());
$('#recipe_id').data('combobox').refresh();
$('#recipe_id').trigger('change');
}
else
{
$('#recipe_id').val(null);
$('#recipe_id_text_input').val("");
$('#recipe_id').data('combobox').refresh();
$('#recipe_id').trigger('change');
}
}
});

View File

@ -386,3 +386,18 @@ $(window).on("message", function(e)
); );
} }
}); });
$(document).on('click', '.recipe-grocycode-label-print', function(e)
{
e.preventDefault();
document.activeElement.blur();
var recipeId = $(e.currentTarget).attr('data-recipe-id');
Grocy.Api.Get('recipes/' + recipeId + '/printlabel', function(labelData)
{
if (Grocy.Webhooks.labelprinter !== undefined)
{
Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, labelData);
}
});
});

View File

@ -414,3 +414,18 @@ if (window.location.hash === "#fullscreen")
} }
LoadImagesLazy(); LoadImagesLazy();
$(document).on('click', '.recipe-grocycode-label-print', function(e)
{
e.preventDefault();
document.activeElement.blur();
var recipeId = $(e.currentTarget).attr('data-recipe-id');
Grocy.Api.Get('recipes/' + recipeId + '/printlabel', function(labelData)
{
if (Grocy.Webhooks.labelprinter !== undefined)
{
Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, labelData);
}
});
});

View File

@ -90,6 +90,7 @@ $app->group('', function (RouteCollectorProxy $group) {
$group->get('/mealplansections', '\Grocy\Controllers\RecipesController:MealPlanSectionsList'); $group->get('/mealplansections', '\Grocy\Controllers\RecipesController:MealPlanSectionsList');
$group->get('/mealplansection/{sectionId}', '\Grocy\Controllers\RecipesController:MealPlanSectionEditForm'); $group->get('/mealplansection/{sectionId}', '\Grocy\Controllers\RecipesController:MealPlanSectionEditForm');
$group->get('/recipessettings', '\Grocy\Controllers\RecipesController:RecipesSettings'); $group->get('/recipessettings', '\Grocy\Controllers\RecipesController:RecipesSettings');
$group->get('/recipe/{recipeId}/grocycode', '\Grocy\Controllers\RecipesController:RecipeGrocycodeImage');
} }
// Chore routes // Chore routes
@ -230,6 +231,8 @@ $app->group('/api', function (RouteCollectorProxy $group) {
$group->post('/recipes/{recipeId}/consume', '\Grocy\Controllers\RecipesApiController:ConsumeRecipe'); $group->post('/recipes/{recipeId}/consume', '\Grocy\Controllers\RecipesApiController:ConsumeRecipe');
$group->get('/recipes/fulfillment', '\Grocy\Controllers\RecipesApiController:GetRecipeFulfillment'); $group->get('/recipes/fulfillment', '\Grocy\Controllers\RecipesApiController:GetRecipeFulfillment');
$group->Post('/recipes/{recipeId}/copy', '\Grocy\Controllers\RecipesApiController:CopyRecipe'); $group->Post('/recipes/{recipeId}/copy', '\Grocy\Controllers\RecipesApiController:CopyRecipe');
$group->get('/recipes/{recipeId}/printlabel', '\Grocy\Controllers\RecipesApiController:RecipePrintLabel');
// Chores // Chores
$group->get('/chores', '\Grocy\Controllers\ChoresApiController:Current'); $group->get('/chores', '\Grocy\Controllers\ChoresApiController:Current');

View File

@ -150,7 +150,7 @@
<span class="dropdown-item-text">{{ $__t('Edit battery') }}</span> <span class="dropdown-item-text">{{ $__t('Edit battery') }}</span>
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item stockentry-grocycode-link" <a class="dropdown-item"
type="button" type="button"
href="{{ $U('/battery/' . $currentBatteryEntry->battery_id . '/grocycode?download=true') }}"> href="{{ $U('/battery/' . $currentBatteryEntry->battery_id . '/grocycode?download=true') }}">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Battery'))) !!} {!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Battery'))) !!}

View File

@ -182,7 +182,7 @@
<span class="dropdown-item-text">{{ $__t('Edit chore') }}</span> <span class="dropdown-item-text">{{ $__t('Edit chore') }}</span>
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item stockentry-grocycode-link" <a class="dropdown-item"
type="button" type="button"
href="{{ $U('/chore/' . $curentChoreEntry->chore_id . '/grocycode?download=true') }}"> href="{{ $U('/chore/' . $curentChoreEntry->chore_id . '/grocycode?download=true') }}">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Chore'))) !!} {!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Chore'))) !!}

View File

@ -14,13 +14,15 @@
data-next-input-selector="{{ $nextInputSelector }}" data-next-input-selector="{{ $nextInputSelector }}"
data-prefill-by-name="{{ $prefillByName }}" data-prefill-by-name="{{ $prefillByName }}"
data-prefill-by-id="{{ $prefillById }}"> data-prefill-by-id="{{ $prefillById }}">
<label for="recipe_id">{{ $__t('Recipe') }} <label class="w-100"
for="recipe_id">{{ $__t('Recipe') }}
@if(!empty($hint)) @if(!empty($hint))
<i class="fas fa-question-circle text-muted" <i class="fas fa-question-circle text-muted"
data-toggle="tooltip" data-toggle="tooltip"
data-trigger="hover click" data-trigger="hover click"
title="{{ $hint }}"></i> title="{{ $hint }}"></i>
@endif @endif
<i class="fas fa-barcode float-right mt-1"></i>
</label> </label>
<select class="form-control recipe-combobox" <select class="form-control recipe-combobox"
id="recipe_id" id="recipe_id"

View File

@ -336,6 +336,37 @@
@endif @endif
</div> </div>
</div> </div>
<div class="row">
<div class="col">
<div class="title-related-links">
<h4>
<span class="ls-n1">{{ $__t('grocycode') }}</span>
<i class="fas fa-question-circle text-muted"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('grocycode is a unique referer to this %s in your grocy instance - print it onto a label and scan it like any other barcode', $__t('Recipe')) }}"></i>
</h4>
<p>
@if($mode == 'edit')
<img src="{{ $U('/recipe/' . $recipe->id . '/grocycode?size=60') }}"
class="float-lg-left">
@endif
</p>
<p>
<a class="btn btn-outline-primary btn-sm"
href="{{ $U('/recipe/' . $recipe->id . '/grocycode?download=true') }}">{{ $__t('Download') }}</a>
@if(GROCY_FEATURE_FLAG_LABEL_PRINTER)
<a class="btn btn-outline-primary btn-sm recipe-grocycode-label-print"
data-recipe-id="{{ $recipe->id }}"
href="#">
{{ $__t('Print on label printer') }}
</a>
@endif
</p>
</div>
</div>
</div>
</div> </div>
</div> </div>

View File

@ -162,6 +162,20 @@
data-recipe-id="{{ $recipe->id }}"> data-recipe-id="{{ $recipe->id }}">
<span class="dropdown-item-text">{{ $__t('Copy recipe') }}</span> <span class="dropdown-item-text">{{ $__t('Copy recipe') }}</span>
</a> </a>
<div class="dropdown-divider"></div>
<a class="dropdown-item"
type="button"
href="{{ $U('/recipe/' . $recipe->id . '/grocycode?download=true') }}">
<span class="dropdown-item-text">{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Recipe'))) !!}</span>
</a>
@if(GROCY_FEATURE_FLAG_LABEL_PRINTER)
<a class="dropdown-item recipe-grocycode-label-print"
data-recipe-id="{{ $recipe->id }}"
type="button"
href="#">
<span class="dropdown-item-text">{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Print %s grocycode on label printer', $__t('Recipe'))) !!}</span>
</a>
@endif
</div> </div>
</div> </div>
</td> </td>
@ -281,7 +295,7 @@
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h3 class="card-title mb-0">{{ $recipe->name }}</h3> <h3 class="card-title mb-0">{{ $recipe->name }}</h3>
<div class="card-icons d-flex flex-wrap justify-content-end flex-shrink-1"> <div class="card-icons d-flex flex-wrap justify-content-end flex-shrink-1">
<a class="@if(!GROCY_FEATURE_FLAG_STOCK) d-none @endif recipe-consume hide-when-embedded @if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 0) disabled @endif" <a class="@if(!GROCY_FEATURE_FLAG_STOCK) d-none @endif recipe-consume @if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 0) disabled @endif"
href="#" href="#"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('Consume all ingredients needed by this recipe') }}" title="{{ $__t('Consume all ingredients needed by this recipe') }}"
@ -289,7 +303,7 @@
data-recipe-name="{{ $recipe->name }}"> data-recipe-name="{{ $recipe->name }}">
<i class="fas fa-utensils"></i> <i class="fas fa-utensils"></i>
</a> </a>
<a class="@if(!GROCY_FEATURE_FLAG_STOCK) d-none @endif recipe-shopping-list hide-when-embedded @if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1) disabled @endif" <a class="@if(!GROCY_FEATURE_FLAG_STOCK) d-none @endif recipe-shopping-list @if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1) disabled @endif"
href="#" href="#"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('Put missing products on shopping list') }}" title="{{ $__t('Put missing products on shopping list') }}"
@ -304,7 +318,7 @@
title="{{ $__t('Expand to fullscreen') }}"> title="{{ $__t('Expand to fullscreen') }}">
<i class="fas fa-expand-arrows-alt"></i> <i class="fas fa-expand-arrows-alt"></i>
</a> </a>
<a class="recipe-print hide-when-embedded" <a class="recipe-print"
href="#" href="#"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('Print') }}"> title="{{ $__t('Print') }}">
@ -317,7 +331,7 @@
<div class="mb-4 @if(!empty($recipe->picture_file_name)) d-none @else d-flex @endif d-print-block justify-content-between align-items-center"> <div class="mb-4 @if(!empty($recipe->picture_file_name)) d-none @else d-flex @endif d-print-block justify-content-between align-items-center">
<h1 class="card-title mb-0">{{ $recipe->name }}</h1> <h1 class="card-title mb-0">{{ $recipe->name }}</h1>
<div class="card-icons d-flex flex-wrap justify-content-end flex-shrink-1 d-print-none"> <div class="card-icons d-flex flex-wrap justify-content-end flex-shrink-1 d-print-none">
<a class="recipe-consume hide-when-embedded @if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 0) disabled @endif" <a class="recipe-consume @if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 0) disabled @endif"
href="#" href="#"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('Consume all ingredients needed by this recipe') }}" title="{{ $__t('Consume all ingredients needed by this recipe') }}"
@ -325,7 +339,7 @@
data-recipe-name="{{ $recipe->name }}"> data-recipe-name="{{ $recipe->name }}">
<i class="fas fa-utensils"></i> <i class="fas fa-utensils"></i>
</a> </a>
<a class="recipe-shopping-list hide-when-embedded @if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1) disabled @endif" <a class="recipe-shopping-list @if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1) disabled @endif"
href="#" href="#"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('Put missing products on shopping list') }}" title="{{ $__t('Put missing products on shopping list') }}"
@ -339,7 +353,7 @@
title="{{ $__t('Expand to fullscreen') }}"> title="{{ $__t('Expand to fullscreen') }}">
<i class="fas fa-expand-arrows-alt"></i> <i class="fas fa-expand-arrows-alt"></i>
</a> </a>
<a class="recipe-print hide-when-embedded PrintRecipe" <a class="recipe-print PrintRecipe"
href="#" href="#"
data-toggle="tooltip" data-toggle="tooltip"
title="{{ $__t('Print') }}"> title="{{ $__t('Print') }}">
@ -355,7 +369,7 @@
<div class="row ml-1"> <div class="row ml-1">
@if(!empty($calories) && intval($calories) > 0) @if(!empty($calories) && intval($calories) > 0)
<div class="col-6 col-xl-3"> <div class="col-4">
<label>{{ $__t('Energy (kcal)') }}</label>&nbsp; <label>{{ $__t('Energy (kcal)') }}</label>&nbsp;
<i class="fas fa-question-circle text-muted d-print-none" <i class="fas fa-question-circle text-muted d-print-none"
data-toggle="tooltip" data-toggle="tooltip"
@ -365,7 +379,7 @@
</div> </div>
@endif @endif
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) @if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<div class="col-5"> <div class="col-4">
<label>{{ $__t('Costs') }}&nbsp; <label>{{ $__t('Costs') }}&nbsp;
<i class="fas fa-question-circle text-muted d-print-none" <i class="fas fa-question-circle text-muted d-print-none"
data-toggle="tooltip" data-toggle="tooltip"
@ -377,7 +391,7 @@
@endif @endif
@if($index == 0) @if($index == 0)
<div class="col-12 col-xl-4 d-print-none"> <div class="col-4 d-print-none">
@include('components.numberpicker', array( @include('components.numberpicker', array(
'id' => 'servings-scale', 'id' => 'servings-scale',
'label' => 'Desired servings', 'label' => 'Desired servings',

View File

@ -201,7 +201,7 @@
{{ $__t('Edit product') }} {{ $__t('Edit product') }}
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item stockentry-grocycode-link" <a class="dropdown-item"
type="button" type="button"
href="{{ $U('/stockentry/' . $stockEntry->id . '/grocycode?download=true') }}"> href="{{ $U('/stockentry/' . $stockEntry->id . '/grocycode?download=true') }}">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Stock entry'))) !!} {!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Stock entry'))) !!}

View File

@ -297,7 +297,7 @@
<span class="dropdown-item-text">{{ $__t('Edit product') }}</span> <span class="dropdown-item-text">{{ $__t('Edit product') }}</span>
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item stockentry-grocycode-link" <a class="dropdown-item"
type="button" type="button"
href="{{ $U('/product/' . $currentStockEntry->product_id . '/grocycode?download=true') }}"> href="{{ $U('/product/' . $currentStockEntry->product_id . '/grocycode?download=true') }}">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Product'))) !!} {!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Product'))) !!}