mirror of
https://github.com/grocy/grocy.git
synced 2026-10-17 13:19:19 +02:00
New API endpoint for copying a product (fixes #2789)
This commit is contained in:
parent
85dc601b37
commit
3c8edb515c
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
### Stock
|
||||
|
||||
- xxx
|
||||
- Fixed that certain fields were not copied when copying a product
|
||||
|
||||
### Shopping list
|
||||
|
||||
|
|
@ -55,4 +55,5 @@
|
|||
|
||||
### API
|
||||
|
||||
- New API endpoint `/stock/products/{productId}/copy` (to copy a product)
|
||||
- Fixed that API Key Authentication did not work when running Grocy in a subdirectory
|
||||
|
|
|
|||
|
|
@ -926,4 +926,25 @@ class StockApiController extends BaseApiController
|
|||
return $this->GenericErrorResponse($response, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function CopyProduct(Request $request, Response $response, array $args)
|
||||
{
|
||||
User::CheckPermission($request, User::PERMISSION_STOCK_EDIT);
|
||||
|
||||
try
|
||||
{
|
||||
if (filter_var($args['productId'], FILTER_VALIDATE_INT) === false)
|
||||
{
|
||||
throw new \Exception('Provided {productId} is not a valid integer');
|
||||
}
|
||||
|
||||
return $this->ApiResponse($response, [
|
||||
'created_object_id' => StockService::GetInstance()->CopyProduct($args['productId'])
|
||||
]);
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->GenericErrorResponse($response, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2333,6 +2333,53 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/stock/products/{productId}/copy": {
|
||||
"post": {
|
||||
"summary": "Copies a product",
|
||||
"tags": [
|
||||
"Stock"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "productId",
|
||||
"required": true,
|
||||
"description": "A valid product id of the product to copy",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The operation was successful",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_object_id": {
|
||||
"type": "integer",
|
||||
"description": "The id of the created product"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "The operation was not successful (possible errors are: Invalid product id)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error400"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/stock/products/{productIdToKeep}/merge/{productIdToRemove}": {
|
||||
"post": {
|
||||
"summary": "Merges two products into one",
|
||||
|
|
|
|||
|
|
@ -2113,9 +2113,6 @@ msgstr[1] ""
|
|||
msgid "Display product"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy recipe"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy of %s"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -392,86 +392,7 @@ $(window).on("message", function (e)
|
|||
}
|
||||
});
|
||||
|
||||
if (Grocy.EditMode == "create" && GetUriParam("copy-of") != undefined)
|
||||
{
|
||||
Grocy.Api.Get('objects/products/' + GetUriParam("copy-of"),
|
||||
function (sourceProduct)
|
||||
{
|
||||
if (sourceProduct.parent_product_id != null)
|
||||
{
|
||||
Grocy.Components.ProductPicker.SetId(sourceProduct.parent_product_id);
|
||||
}
|
||||
if (sourceProduct.description)
|
||||
{
|
||||
$("#description").summernote("pasteHTML", sourceProduct.description);
|
||||
}
|
||||
$("#location_id").val(sourceProduct.location_id);
|
||||
if (sourceProduct.shopping_location_id != null)
|
||||
{
|
||||
Grocy.Components.ShoppingLocationPicker.SetId(sourceProduct.shopping_location_id);
|
||||
}
|
||||
$("#min_stock_amount").val(sourceProduct.min_stock_amount);
|
||||
if (BoolVal(sourceProduct.cumulate_min_stock_amount_of_sub_products))
|
||||
{
|
||||
$("#cumulate_min_stock_amount_of_sub_products").prop("checked", true);
|
||||
}
|
||||
$("#default_best_before_days").val(sourceProduct.default_best_before_days);
|
||||
$("#default_best_before_days_after_open").val(sourceProduct.default_best_before_days_after_open);
|
||||
if (sourceProduct.product_group_id != null)
|
||||
{
|
||||
$("#product_group_id").val(sourceProduct.product_group_id);
|
||||
}
|
||||
$("#qu_id_stock").val(sourceProduct.qu_id_stock);
|
||||
$("#qu_id_purchase").val(sourceProduct.qu_id_purchase);
|
||||
if (BoolVal(sourceProduct.enable_tare_weight_handling))
|
||||
{
|
||||
$("#enable_tare_weight_handling").prop("checked", true);
|
||||
}
|
||||
$("#tare_weight").val(sourceProduct.tare_weight);
|
||||
if (BoolVal(sourceProduct.not_check_stock_fulfillment_for_recipes))
|
||||
{
|
||||
$("#not_check_stock_fulfillment_for_recipes").prop("checked", true);
|
||||
}
|
||||
if (sourceProduct.calories != null)
|
||||
{
|
||||
$("#calories").val(sourceProduct.calories);
|
||||
}
|
||||
$("#default_best_before_days_after_freezing").val(sourceProduct.default_best_before_days_after_freezing);
|
||||
$("#default_best_before_days_after_thawing").val(sourceProduct.default_best_before_days_after_thawing);
|
||||
$("#quick_consume_amount").val(sourceProduct.quick_consume_amount);
|
||||
$("#quick_open_amount").val(sourceProduct.quick_open_amount);
|
||||
$("#default_consume_location_id").val(sourceProduct.default_consume_location_id);
|
||||
if (BoolVal(sourceProduct.no_own_stock))
|
||||
{
|
||||
$("#no_own_stock").prop("checked", true);
|
||||
}
|
||||
if (BoolVal(sourceProduct.hide_on_stock_overview))
|
||||
{
|
||||
$("#hide_on_stock_overview").prop("checked", true);
|
||||
}
|
||||
if (BoolVal(sourceProduct.auto_reprint_stock_label))
|
||||
{
|
||||
$("#auto_reprint_stock_label").prop("checked", true);
|
||||
}
|
||||
$("#default_stock_label_type").val(sourceProduct.default_stock_label_type);
|
||||
if (BoolVal(sourceProduct.move_on_open))
|
||||
{
|
||||
$("#move_on_open").prop("checked", true);
|
||||
}
|
||||
if (BoolVal(sourceProduct.treat_opened_as_out_of_stock))
|
||||
{
|
||||
$("#treat_opened_as_out_of_stock").prop("checked", true);
|
||||
}
|
||||
|
||||
Grocy.FrontendHelpers.ValidateForm('product-form');
|
||||
},
|
||||
function (xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
}
|
||||
else if (Grocy.EditMode === 'create')
|
||||
if (Grocy.EditMode === 'create')
|
||||
{
|
||||
if (Grocy.UserSettings.product_presets_location_id.toString() !== '-1')
|
||||
{
|
||||
|
|
|
|||
|
|
@ -175,3 +175,21 @@ $("#merge-products-save-button").on("click", function (e)
|
|||
}
|
||||
);
|
||||
});
|
||||
|
||||
$(".product-copy-button").on('click', function (e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
var objectId = $(e.currentTarget).attr("data-product-id");
|
||||
|
||||
Grocy.Api.Post("stock/products/" + objectId.toString() + "/copy", {},
|
||||
function (result)
|
||||
{
|
||||
window.location.href = U("/product/" + result.created_object_id.toString());
|
||||
},
|
||||
function (xhr)
|
||||
{
|
||||
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -207,6 +207,7 @@ $app->group('/api', function (RouteCollectorProxy $group)
|
|||
$group->post('/stock/products/{productId}/inventory', [StockApiController::class, 'InventoryProduct']);
|
||||
$group->post('/stock/products/{productId}/open', [StockApiController::class, 'OpenProduct']);
|
||||
$group->post('/stock/products/{productIdToKeep}/merge/{productIdToRemove}', [StockApiController::class, 'MergeProducts']);
|
||||
$group->post('/stock/products/{productId}/copy', [StockApiController::class, 'CopyProduct']);
|
||||
$group->get('/stock/products/by-barcode/{barcode}', [StockApiController::class, 'ProductDetailsByBarcode']);
|
||||
$group->post('/stock/products/by-barcode/{barcode}/add', [StockApiController::class, 'AddProductByBarcode']);
|
||||
$group->post('/stock/products/by-barcode/{barcode}/consume', [StockApiController::class, 'ConsumeProductByBarcode']);
|
||||
|
|
|
|||
|
|
@ -1730,6 +1730,54 @@ class StockService extends BaseService
|
|||
DatabaseService::GetInstance()->GetDbConnectionRaw()->commit();
|
||||
}
|
||||
|
||||
public function CopyProduct(int $productId)
|
||||
{
|
||||
if (!$this->ProductExists($productId))
|
||||
{
|
||||
throw new \Exception('Product does not exist');
|
||||
}
|
||||
|
||||
$newName = LocalizationService::GetInstance()->__t('Copy of %s', $this->DB->products($productId)->name);
|
||||
|
||||
DatabaseService::GetInstance()->ExecuteDbStatement(
|
||||
'INSERT INTO products
|
||||
(name, description, product_group_id, active, location_id, shopping_location_id, qu_id_purchase, qu_id_stock, min_stock_amount,
|
||||
default_best_before_days, default_best_before_days_after_open, default_best_before_days_after_freezing,
|
||||
default_best_before_days_after_thawing, picture_file_name, enable_tare_weight_handling, tare_weight, not_check_stock_fulfillment_for_recipes,
|
||||
parent_product_id, calories, cumulate_min_stock_amount_of_sub_products, due_type, quick_consume_amount, hide_on_stock_overview, default_stock_label_type,
|
||||
should_not_be_frozen, treat_opened_as_out_of_stock, no_own_stock, default_consume_location_id, move_on_open, row_created_timestamp, qu_id_consume,
|
||||
auto_reprint_stock_label, quick_open_amount, qu_id_price, disable_open, default_purchase_price_type)
|
||||
SELECT :new_name, description, product_group_id, active, location_id, shopping_location_id, qu_id_purchase, qu_id_stock, min_stock_amount,
|
||||
default_best_before_days, default_best_before_days_after_open, default_best_before_days_after_freezing,
|
||||
default_best_before_days_after_thawing, picture_file_name, enable_tare_weight_handling, tare_weight, not_check_stock_fulfillment_for_recipes,
|
||||
parent_product_id, calories, cumulate_min_stock_amount_of_sub_products, due_type, quick_consume_amount, hide_on_stock_overview, default_stock_label_type,
|
||||
should_not_be_frozen, treat_opened_as_out_of_stock, no_own_stock, default_consume_location_id, move_on_open, row_created_timestamp, qu_id_consume,
|
||||
auto_reprint_stock_label, quick_open_amount, qu_id_price, disable_open, default_purchase_price_type
|
||||
FROM products
|
||||
WHERE id = :product_id',
|
||||
['product_id' => $productId, 'new_name' => $newName]
|
||||
);
|
||||
$lastInsertId = $this->DB->lastInsertId();
|
||||
|
||||
DatabaseService::GetInstance()->ExecuteDbStatement(
|
||||
'INSERT INTO quantity_unit_conversions
|
||||
(product_id, from_qu_id, to_qu_id, factor)
|
||||
SELECT :last_insert_id, from_qu_id, to_qu_id, factor
|
||||
FROM quantity_unit_conversions q
|
||||
WHERE IFNULL(q.product_id, 0) = :product_id
|
||||
AND q.id = (
|
||||
SELECT MIN(q2.id)
|
||||
FROM quantity_unit_conversions q2
|
||||
WHERE IFNULL(q2.product_id, 0) = :product_id
|
||||
AND MIN(q2.from_qu_id, q2.to_qu_id) = MIN(q.from_qu_id, q.to_qu_id)
|
||||
AND MAX(q2.from_qu_id, q2.to_qu_id) = MAX(q.from_qu_id, q.to_qu_id)
|
||||
)', // The corresponding inverse conversion is automatically created by a trigger
|
||||
['product_id' => $productId, 'last_insert_id' => $lastInsertId]
|
||||
);
|
||||
|
||||
return $lastInsertId;
|
||||
}
|
||||
|
||||
public function CompactStockEntries($productId = null)
|
||||
{
|
||||
if ($productId == null)
|
||||
|
|
|
|||
|
|
@ -160,9 +160,10 @@
|
|||
<i class="fa-solid fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<div class="table-inline-menu dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item"
|
||||
<a class="dropdown-item product-copy-button"
|
||||
data-product-id="{{ $product->id }}"
|
||||
type="button"
|
||||
href="{{ $U('/product/new?copy-of=') }}{{ $product->id }}">
|
||||
href="#">
|
||||
<span class="dropdown-item-text">{{ $__t('Copy') }}</span>
|
||||
</a>
|
||||
<a class="dropdown-item merge-products-button"
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@
|
|||
type="button"
|
||||
href="#"
|
||||
data-recipe-id="{{ $recipe->id }}">
|
||||
<span class="dropdown-item-text">{{ $__t('Copy recipe') }}</span>
|
||||
<span class="dropdown-item-text">{{ $__t('Copy') }}</span>
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user