mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 09:39:57 +00:00
Implemented product variations (closes #196)
This commit is contained in:
parent
26ebeec74f
commit
a0a0e104b0
@ -1,5 +1,11 @@
|
||||
- Stock improvements
|
||||
- You can now print a "Location Content Sheet" with the current stock per location - new button at the top of the stock overview page
|
||||
- Products can now have variations
|
||||
- Define the parent product for a product on the product edit page (only one level is possible, means a product which is used as a parent product in another product, cannot have a parent product itself)
|
||||
- Parent and sub products can have stock (both are regular products, no difference from that side)
|
||||
- On the stock overview page the aggregated amount is displayed next to the amount (sigma sign)
|
||||
- When a recipe needs a parent product, the need is also fulfilled when enough sub product(s) are in stock
|
||||
- API change (no breaking change): `/stock/products/{productId}` returns additional fields for the aggregated up amount(s): `stock_amount_aggregated` and `stock_amount_opened_aggregated` - contains the same for "normal" products, `is_aggregated_amount` indicates if aggregation has happened
|
||||
- It's now possible to print a "Location Content Sheet" with the current stock per location - new button at the top of the stock overview page
|
||||
- The product description now can have formattings (HTML/WYSIWYG editor like for recipes)
|
||||
- Chores improvements
|
||||
- New option "Due date rollover" per chore which means the chore can never be overdue, the due date will shift forward each day when due
|
||||
|
@ -139,17 +139,23 @@ class StockController extends BaseController
|
||||
'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
|
||||
'productgroups' => $this->Database->product_groups()->orderBy('name'),
|
||||
'userfields' => $this->UserfieldsService->GetFields('products'),
|
||||
'products' => $this->Database->products()->where('parent_product_id IS NULL')->orderBy('name'),
|
||||
'isSubProductOfOthers' => false,
|
||||
'mode' => 'create'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
$product = $this->Database->products($args['productId']);
|
||||
|
||||
return $this->AppContainer->view->render($response, 'productform', [
|
||||
'product' => $this->Database->products($args['productId']),
|
||||
'product' => $product,
|
||||
'locations' => $this->Database->locations()->orderBy('name'),
|
||||
'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
|
||||
'productgroups' => $this->Database->product_groups()->orderBy('name'),
|
||||
'userfields' => $this->UserfieldsService->GetFields('products'),
|
||||
'products' => $this->Database->products()->where('id != :1 AND parent_product_id IS NULL', $product->id)->orderBy('name'),
|
||||
'isSubProductOfOthers' => $this->Database->products()->where('parent_product_id = :1', $product->id)->count() !== 0,
|
||||
'mode' => 'edit'
|
||||
]);
|
||||
}
|
||||
|
@ -277,3 +277,9 @@ msgstr ""
|
||||
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
msgid "Milk Chocolate"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dark Chocolate"
|
||||
msgstr ""
|
||||
|
@ -1316,3 +1316,9 @@ msgstr ""
|
||||
|
||||
msgid "Are you sure to delete equipment \"%s\"?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Parent product"
|
||||
msgstr ""
|
||||
|
||||
msgid "Not possible because this product is already used as a parent product in another product"
|
||||
msgstr ""
|
||||
|
158
migrations/0081.sql
Normal file
158
migrations/0081.sql
Normal file
@ -0,0 +1,158 @@
|
||||
ALTER TABLE products
|
||||
ADD parent_product_id INT;
|
||||
|
||||
CREATE VIEW products_resolved
|
||||
AS
|
||||
SELECT
|
||||
p.parent_product_id parent_product_id,
|
||||
p.id as sub_product_id
|
||||
FROM products p
|
||||
WHERE p.parent_product_id IS NOT NULL
|
||||
|
||||
UNION
|
||||
|
||||
SELECT
|
||||
p.id parent_product_id,
|
||||
p.id as sub_product_id
|
||||
FROM products p
|
||||
WHERE p.parent_product_id IS NULL;
|
||||
|
||||
DROP VIEW stock_current;
|
||||
CREATE VIEW stock_current
|
||||
AS
|
||||
SELECT
|
||||
pr.parent_product_id AS product_id,
|
||||
IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = pr.parent_product_id), 0) AS amount,
|
||||
SUM(s.amount) AS amount_aggregated,
|
||||
MIN(s.best_before_date) AS best_before_date,
|
||||
IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = pr.parent_product_id AND open = 1), 0) AS amount_opened,
|
||||
IFNULL((SELECT SUM(amount) FROM stock WHERE product_id IN (SELECT sub_product_id FROM products_resolved WHERE parent_product_id = pr.parent_product_id) AND open = 1), 0) AS amount_opened_aggregated,
|
||||
CASE WHEN p.parent_product_id IS NOT NULL THEN 1 ELSE 0 END AS is_aggregated_amount
|
||||
FROM products_resolved pr
|
||||
JOIN stock s
|
||||
ON pr.sub_product_id = s.product_id
|
||||
JOIN products p
|
||||
ON pr.sub_product_id = p.id
|
||||
GROUP BY pr.parent_product_id
|
||||
HAVING SUM(s.amount) > 0
|
||||
|
||||
UNION
|
||||
|
||||
-- This is the same as above but sub products not rolled up (column is_aggregated_amount = 0 here)
|
||||
SELECT
|
||||
pr.sub_product_id AS product_id,
|
||||
SUM(s.amount) AS amount,
|
||||
SUM(s.amount) AS amount_aggregated,
|
||||
MIN(s.best_before_date) AS best_before_date,
|
||||
IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = s.product_id AND open = 1), 0) AS amount_opened,
|
||||
IFNULL((SELECT SUM(amount) FROM stock WHERE product_id = s.product_id AND open = 1), 0) AS amount_opened_aggregated,
|
||||
0 AS is_aggregated_amount
|
||||
FROM products_resolved pr
|
||||
JOIN stock s
|
||||
ON pr.sub_product_id = s.product_id
|
||||
WHERE pr.parent_product_id != pr.sub_product_id
|
||||
GROUP BY pr.sub_product_id
|
||||
HAVING SUM(s.amount) > 0;
|
||||
|
||||
DROP VIEW stock_missing_products;
|
||||
CREATE VIEW stock_missing_products
|
||||
AS
|
||||
SELECT
|
||||
p.id,
|
||||
MAX(p.name) AS name,
|
||||
p.min_stock_amount - IFNULL(SUM(s.amount), 0) AS amount_missing,
|
||||
CASE WHEN IFNULL(SUM(s.amount), 0) > 0 THEN 1 ELSE 0 END AS is_partly_in_stock
|
||||
FROM products p
|
||||
LEFT JOIN stock_current s
|
||||
ON p.id = s.product_id
|
||||
WHERE p.min_stock_amount != 0
|
||||
GROUP BY p.id
|
||||
HAVING IFNULL(SUM(s.amount), 0) < p.min_stock_amount;
|
||||
|
||||
DROP VIEW recipes_pos_resolved;
|
||||
CREATE VIEW recipes_pos_resolved
|
||||
AS
|
||||
|
||||
-- Multiplication by 1.0 to force conversion to float (REAL)
|
||||
|
||||
SELECT
|
||||
r.id AS recipe_id,
|
||||
rp.id AS recipe_pos_id,
|
||||
rp.product_id AS product_id,
|
||||
rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount,
|
||||
IFNULL(sc.amount_aggregated, 0) AS stock_amount,
|
||||
CASE WHEN IFNULL(sc.amount_aggregated, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled,
|
||||
CASE WHEN IFNULL(sc.amount_aggregated, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END < 0 THEN ABS(IFNULL(sc.amount_aggregated, 0) - (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END)) ELSE 0 END AS missing_amount,
|
||||
IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list,
|
||||
CASE WHEN IFNULL(sc.amount_aggregated, 0) + (CASE WHEN r.not_check_shoppinglist = 1 THEN 0 ELSE IFNULL(sl.amount, 0) END * p.qu_factor_purchase_to_stock) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list,
|
||||
rp.qu_id,
|
||||
(CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END / p.qu_factor_purchase_to_stock) * pcp.last_price AS costs,
|
||||
CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos,
|
||||
rp.ingredient_group,
|
||||
rp.id, -- Just a dummy id column
|
||||
rnr.includes_recipe_id as child_recipe_id,
|
||||
rp.note,
|
||||
rp.variable_amount AS recipe_variable_amount,
|
||||
rp.only_check_single_unit_in_stock
|
||||
FROM recipes r
|
||||
JOIN recipes_nestings_resolved rnr
|
||||
ON r.id = rnr.recipe_id
|
||||
JOIN recipes rnrr
|
||||
ON rnr.includes_recipe_id = rnrr.id
|
||||
JOIN recipes_pos rp
|
||||
ON rnr.includes_recipe_id = rp.recipe_id
|
||||
JOIN products p
|
||||
ON rp.product_id = p.id
|
||||
LEFT JOIN (
|
||||
SELECT product_id, SUM(amount) AS amount
|
||||
FROM shopping_list
|
||||
GROUP BY product_id) sl
|
||||
ON rp.product_id = sl.product_id
|
||||
LEFT JOIN stock_current sc
|
||||
ON rp.product_id = sc.product_id
|
||||
LEFT JOIN products_current_price pcp
|
||||
ON rp.product_id = pcp.product_id
|
||||
WHERE rp.not_check_stock_fulfillment = 0
|
||||
|
||||
UNION
|
||||
|
||||
-- Just add all recipe positions which should not be checked against stock with fulfilled need
|
||||
|
||||
SELECT
|
||||
r.id AS recipe_id,
|
||||
rp.id AS recipe_pos_id,
|
||||
rp.product_id AS product_id,
|
||||
rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount,
|
||||
IFNULL(sc.amount_aggregated, 0) AS stock_amount,
|
||||
1 AS need_fulfilled,
|
||||
0 AS missing_amount,
|
||||
IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list,
|
||||
1 AS need_fulfilled_with_shopping_list,
|
||||
rp.qu_id,
|
||||
(CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END / p.qu_factor_purchase_to_stock) * pcp.last_price AS costs,
|
||||
CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos,
|
||||
rp.ingredient_group,
|
||||
rp.id, -- Just a dummy id column
|
||||
rnr.includes_recipe_id as child_recipe_id,
|
||||
rp.note,
|
||||
rp.variable_amount AS recipe_variable_amount,
|
||||
rp.only_check_single_unit_in_stock
|
||||
FROM recipes r
|
||||
JOIN recipes_nestings_resolved rnr
|
||||
ON r.id = rnr.recipe_id
|
||||
JOIN recipes rnrr
|
||||
ON rnr.includes_recipe_id = rnrr.id
|
||||
JOIN recipes_pos rp
|
||||
ON rnr.includes_recipe_id = rp.recipe_id
|
||||
JOIN products p
|
||||
ON rp.product_id = p.id
|
||||
LEFT JOIN (
|
||||
SELECT product_id, SUM(amount) AS amount
|
||||
FROM shopping_list
|
||||
GROUP BY product_id) sl
|
||||
ON rp.product_id = sl.product_id
|
||||
LEFT JOIN stock_current sc
|
||||
ON rp.product_id = sc.product_id
|
||||
LEFT JOIN products_current_price pcp
|
||||
ON rp.product_id = pcp.product_id
|
||||
WHERE rp.not_check_stock_fulfillment = 1;
|
@ -252,6 +252,12 @@ input::-webkit-inner-spin-button {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Third party component customizations - Font Awesome */
|
||||
.fa-custom-sigma-sign:before {
|
||||
content: "\03a3";
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
/* Third party component customizations - SB Admin 2 */
|
||||
#mainNav .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link:after,
|
||||
#mainNav .navbar-collapse .navbar-sidenav .nav-link-collapse:after {
|
||||
|
@ -136,7 +136,7 @@ $('#product_id_text_input').on('blur', function(e)
|
||||
}
|
||||
|
||||
var optionElement = $("#product_id option:contains(\"" + input + "\")").first();
|
||||
if (input.length > 0 && optionElement.length === 0 && typeof GetUriParam('addbarcodetoselection') === "undefined")
|
||||
if (input.length > 0 && optionElement.length === 0 && typeof GetUriParam('addbarcodetoselection') === "undefined" && Grocy.Components.ProductPicker.GetPicker().parent().data('disallow-all-product-workflows').toString() === "false")
|
||||
{
|
||||
var addProductWorkflowsAdditionalCssClasses = "";
|
||||
if (Grocy.Components.ProductPicker.GetPicker().parent().data('disallow-add-product-workflows').toString() === "true")
|
||||
|
@ -200,7 +200,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
||||
$("#tare-weight-handling-info").addClass("d-none");
|
||||
}
|
||||
|
||||
if ((productDetails.stock_amount || 0) === 0)
|
||||
if ((parseFloat(productDetails.stock_amount) || 0) === 0)
|
||||
{
|
||||
Grocy.Components.ProductPicker.Clear();
|
||||
Grocy.FrontendHelpers.ValidateForm('consume-form');
|
||||
|
@ -10,8 +10,16 @@
|
||||
}
|
||||
|
||||
var jsonData = $('#product-form').serializeJSON({ checkboxUncheckedValue: "0" });
|
||||
var parentProductId = jsonData.product_id;
|
||||
delete jsonData.product_id;
|
||||
jsonData.parent_product_id = parentProductId;
|
||||
Grocy.FrontendHelpers.BeginUiBusy("product-form");
|
||||
|
||||
if (jsonData.parent_product_id.toString().isEmpty())
|
||||
{
|
||||
jsonData.parent_product_id = null;
|
||||
}
|
||||
|
||||
if ($("#product-picture")[0].files.length > 0)
|
||||
{
|
||||
var someRandomStuff = Math.random().toString(36).substring(2, 100) + Math.random().toString(36).substring(2, 100);
|
||||
|
@ -80,6 +80,8 @@ class DemoDataGeneratorService extends BaseService
|
||||
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 ('{$this->__t_sql('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 ('{$this->__t_sql('Sugar')}', 3, 3, 3, 1, 3); --22
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$this->__t_sql('Milk')}', 2, 10, 10, 1, 6); --23
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, parent_product_id) VALUES ('{$this->__t_sql('Milk Chocolate')}', 4, 3, 3, 1, 1, 2); --24
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, parent_product_id) VALUES ('{$this->__t_sql('Dark Chocolate')}', 4, 3, 3, 1, 1, 2); --25
|
||||
|
||||
INSERT INTO shopping_list (note, amount) VALUES ('{$this->__t_sql('Some good snacks')}', 1);
|
||||
INSERT INTO shopping_list (product_id, amount) VALUES (20, 1);
|
||||
@ -222,6 +224,9 @@ class DemoDataGeneratorService extends BaseService
|
||||
$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->AddProduct(24, 2, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice());
|
||||
$stockService->AddProduct(25, 2, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice());
|
||||
$stockService->AddProduct(2, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice());
|
||||
$stockService->AddMissingProductsToShoppingList();
|
||||
$stockService->OpenProduct(3, 1);
|
||||
$stockService->OpenProduct(6, 1);
|
||||
|
@ -11,10 +11,10 @@ class StockService extends BaseService
|
||||
|
||||
public function GetCurrentStock($includeNotInStockButMissingProducts = false)
|
||||
{
|
||||
$sql = 'SELECT * FROM stock_current';
|
||||
$sql = 'SELECT * FROM stock_current UNION SELECT id, 0, 0, null, 0, 0, 0 FROM stock_missing_products WHERE id NOT IN (SELECT product_id FROM stock_current)';
|
||||
if ($includeNotInStockButMissingProducts)
|
||||
{
|
||||
$sql = 'SELECT * FROM stock_current WHERE best_before_date IS NOT NULL';
|
||||
$sql = 'SELECT * FROM stock_current WHERE best_before_date IS NOT NULL UNION SELECT id, 0, 0, null, 0, 0, 0 FROM stock_missing_products WHERE id NOT IN (SELECT product_id FROM stock_current)';
|
||||
}
|
||||
|
||||
return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
|
||||
@ -76,13 +76,9 @@ class StockService extends BaseService
|
||||
throw new \Exception('Product does not exist');
|
||||
}
|
||||
|
||||
$stockCurrentRow = FindObjectinArrayByPropertyValue($this->GetCurrentStock(), 'product_id', $productId);
|
||||
|
||||
$product = $this->Database->products($productId);
|
||||
$productStockAmount = $this->Database->stock()->where('product_id', $productId)->sum('amount');
|
||||
if ($productStockAmount == null)
|
||||
{
|
||||
$productStockAmount = 0;
|
||||
}
|
||||
$productStockAmountOpened = $this->Database->stock()->where('product_id = :1 AND open = 1', $productId)->sum('amount');
|
||||
$productLastPurchased = $this->Database->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_PURCHASE)->where('undone', 0)->max('purchased_date');
|
||||
$productLastUsed = $this->Database->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->where('undone', 0)->max('used_date');
|
||||
$nextBestBeforeDate = $this->Database->stock()->where('product_id', $productId)->min('best_before_date');
|
||||
@ -110,15 +106,18 @@ class StockService extends BaseService
|
||||
'product' => $product,
|
||||
'last_purchased' => $productLastPurchased,
|
||||
'last_used' => $productLastUsed,
|
||||
'stock_amount' => $productStockAmount,
|
||||
'stock_amount_opened' => $productStockAmountOpened,
|
||||
'stock_amount' => $stockCurrentRow->amount,
|
||||
'stock_amount_opened' => $stockCurrentRow->amount_opened,
|
||||
'stock_amount_aggregated' => $stockCurrentRow->amount_aggregated,
|
||||
'stock_amount_opened_aggregated' => $stockCurrentRow->amount_opened_aggregated,
|
||||
'quantity_unit_purchase' => $quPurchase,
|
||||
'quantity_unit_stock' => $quStock,
|
||||
'last_price' => $lastPrice,
|
||||
'next_best_before_date' => $nextBestBeforeDate,
|
||||
'location' => $location,
|
||||
'average_shelf_life_days' => $averageShelfLifeDays,
|
||||
'spoil_rate_percent' => $spoilRate
|
||||
'spoil_rate_percent' => $spoilRate,
|
||||
'is_aggregated_amount' => $stockCurrentRow->is_aggregated_amount
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -3,13 +3,17 @@
|
||||
@endpush
|
||||
|
||||
@php if(empty($disallowAddProductWorkflows)) { $disallowAddProductWorkflows = false; } @endphp
|
||||
@php if(empty($disallowAllProductWorkflows)) { $disallowAllProductWorkflows = false; } @endphp
|
||||
@php if(empty($prefillByName)) { $prefillByName = ''; } @endphp
|
||||
@php if(empty($prefillById)) { $prefillById = ''; } @endphp
|
||||
@php if(!isset($isRequired)) { $isRequired = true; } @endphp
|
||||
@php if(!isset($label)) { $label = 'Product'; } @endphp
|
||||
@php if(!isset($disabled)) { $disabled = false; } @endphp
|
||||
@php if(empty($hint)) { $hint = ''; } @endphp
|
||||
|
||||
<div class="form-group" data-next-input-selector="{{ $nextInputSelector }}" data-disallow-add-product-workflows="{{ BoolToString($disallowAddProductWorkflows) }}" data-prefill-by-name="{{ $prefillByName }}" data-prefill-by-id="{{ $prefillById }}">
|
||||
<label for="product_id">{{ $__t('Product') }} <i class="fas fa-barcode"></i><span id="barcode-lookup-disabled-hint" class="small text-muted d-none"> {{ $__t('Barcode lookup is disabled') }}</span></label>
|
||||
<select class="form-control product-combobox" id="product_id" name="product_id" @if($isRequired) required @endif>
|
||||
<div class="form-group" data-next-input-selector="{{ $nextInputSelector }}" data-disallow-add-product-workflows="{{ BoolToString($disallowAddProductWorkflows) }}" data-disallow-all-product-workflows="{{ BoolToString($disallowAllProductWorkflows) }}" data-prefill-by-name="{{ $prefillByName }}" data-prefill-by-id="{{ $prefillById }}">
|
||||
<label for="product_id">{{ $__t($label) }} <i class="fas fa-barcode"></i><span id="barcode-lookup-disabled-hint" class="small text-muted d-none"> {{ $__t('Barcode lookup is disabled') }}</span> <span class="small text-muted">{{ $hint }}</span></label>
|
||||
<select class="form-control product-combobox" id="product_id" name="product_id" @if($isRequired) required @endif @if($disabled) disabled @endif>
|
||||
<option value=""></option>
|
||||
@foreach($products as $product)
|
||||
<option data-additional-searchdata="{{ $product->barcode }}" value="{{ $product->id }}">{{ $product->name }}</option>
|
||||
@ -17,6 +21,8 @@
|
||||
</select>
|
||||
<div class="invalid-feedback">{{ $__t('You have to select a product') }}</div>
|
||||
<div id="custom-productpicker-error" class="form-text text-danger d-none"></div>
|
||||
<div class="form-text text-info small">{{ $__t('Type a new product name or barcode and hit TAB to start a workflow') }}</div>
|
||||
@if(!$disallowAllProductWorkflows)
|
||||
<div class="form-text text-info small">{{ $__t('Type a new product name or barcode and hit TAB to start a workflow') }}</div>
|
||||
@endif
|
||||
<div id="flow-info-addbarcodetoselection" class="form-text text-muted small d-none"><strong><span id="addbarcodetoselection"></span></strong> {{ $__t('will be added to the list of barcodes for the selected product on submit') }}</div>
|
||||
</div>
|
||||
|
@ -40,6 +40,25 @@
|
||||
<div class="invalid-feedback">{{ $__t('A name is required') }}</div>
|
||||
</div>
|
||||
|
||||
@php $prefillById = ''; if($mode=='edit') { $prefillById = $product->parent_product_id; } @endphp
|
||||
@php
|
||||
$hint = '';
|
||||
if ($isSubProductOfOthers)
|
||||
{
|
||||
$hint = $__t('Not possible because this product is already used as a parent product in another product');
|
||||
}
|
||||
@endphp
|
||||
@include('components.productpicker', array(
|
||||
'products' => $products,
|
||||
'nextInputSelector' => '#barcode-taginput',
|
||||
'prefillById' => $prefillById,
|
||||
'disallowAllProductWorkflows' => true,
|
||||
'isRequired' => false,
|
||||
'label' => 'Parent product',
|
||||
'disabled' => $isSubProductOfOthers,
|
||||
'hint' => $hint
|
||||
))
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">{{ $__t('Description') }}</label>
|
||||
<textarea class="form-control wysiwyg-editor" id="description" name="description">@if($mode == 'edit'){{ $product->description }}@endif</textarea>
|
||||
|
@ -142,6 +142,12 @@
|
||||
<td>
|
||||
<span id="product-{{ $currentStockEntry->product_id }}-amount">{{ $currentStockEntry->amount }}</span> <span id="product-{{ $currentStockEntry->product_id }}-qu-name">{{ $__n($currentStockEntry->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name_plural) }}</span>
|
||||
<span id="product-{{ $currentStockEntry->product_id }}-opened-amount" class="small font-italic">@if($currentStockEntry->amount_opened > 0){{ $__t('%s opened', $currentStockEntry->amount_opened) }}@endif</span>
|
||||
@if($currentStockEntry->is_aggregated_amount == 1)
|
||||
<span class="pl-5">
|
||||
<i class="fas fa-custom-sigma-sign"></i> {{ $currentStockEntry->amount_aggregated }} {{ $__n($currentStockEntry->amount_aggregated, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name_plural) }}
|
||||
@if($currentStockEntry->amount_opened_aggregated > 0)<span class="small font-italic">{{ $__t('%s opened', $currentStockEntry->amount_opened_aggregated) }}</span>@endif
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<span id="product-{{ $currentStockEntry->product_id }}-next-best-before-date">{{ $currentStockEntry->best_before_date }}</span>
|
||||
|
Loading…
x
Reference in New Issue
Block a user