mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 01:32:38 +00:00
Make it possible to merge products (closes #243)
This commit is contained in:
parent
dadf93a94c
commit
c9b5e14473
@ -46,6 +46,8 @@
|
||||
- The amount to be used for the "quick consume/open buttons" on the stock overview page can now be configured per product (new product option "Quick consume amount", defaults to 1)
|
||||
- This "Quick consume amount" can optionally also be used as the default on the consume page (new stock setting / top right corner settings menu)
|
||||
- Products can now be duplicated (new button on the products list page, all fields will be preset from the copied product, except the name)
|
||||
- Products can now be merged (new button on the products list page)
|
||||
- Useful if you have two products which are basically the same and want to replace all occurrences of one with the other one
|
||||
- When consuming or opening a parent product, which is currently not in stock, any in-stock sub product will now be consumed/opened (like already automatically done when consuming recipes)
|
||||
- Opened stock entries get now consumed first by default when no specific stock entry is used/selected
|
||||
- So the default consume rule is now "Opened first, then first due first, then first in first out"
|
||||
@ -241,6 +243,7 @@
|
||||
- New endpoints GET/POST/PUT `/users/{userId}/permissions` for the new user permissions feature mentioned above
|
||||
- New endpoint `/user` to get the currently authenticated user
|
||||
- New endpoint DELETE `/user/settings/{settingKey}` to delete a user setting
|
||||
- New endpoint POST `/stock/products/{productIdToKeep}/merge/{productIdToRemove}` for the new product merging feature mentioned above
|
||||
- The following entities are now also available via the endpoint `/objects/{entity}` (only listing, no edit)
|
||||
- `stock_log` (the stock journal)
|
||||
- `stock` (the "raw" stock entries)
|
||||
|
@ -775,6 +775,26 @@ class StockApiController extends BaseApiController
|
||||
}
|
||||
}
|
||||
|
||||
public function MergeProducts(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
|
||||
{
|
||||
User::checkPermission($request, User::PERMISSION_STOCK_EDIT);
|
||||
|
||||
try
|
||||
{
|
||||
if (!filter_var($args['productIdToKeep'], FILTER_VALIDATE_INT) || !filter_var($args['productIdToRemove'], FILTER_VALIDATE_INT))
|
||||
{
|
||||
throw new \Exception('Provided {productIdToKeep} or {productIdToRemove} is not a valid integer');
|
||||
}
|
||||
|
||||
$this->ApiResponse($response, $this->getStockService()->MergeProducts($args['productIdToKeep'], $args['productIdToRemove']));
|
||||
return $this->EmptyApiResponse($response);
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->GenericErrorResponse($response, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function __construct(\DI\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
|
@ -2178,6 +2178,49 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/stock/products/{productIdToKeep}/merge/{productIdToRemove}": {
|
||||
"post": {
|
||||
"summary": "Merges two products into one",
|
||||
"tags": [
|
||||
"Stock"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "productIdToKeep",
|
||||
"required": true,
|
||||
"description": "A valid product id of the product to keep",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "productIdToRemove",
|
||||
"required": true,
|
||||
"description": "A valid product id of the product to remove",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "The operation was successful"
|
||||
},
|
||||
"400": {
|
||||
"description": "The operation was not successful (possible errors are: Invalid product id)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error400"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/stock/products/by-barcode/{barcode}": {
|
||||
"get": {
|
||||
"summary": "Returns details of the given product by its barcode",
|
||||
|
@ -2071,3 +2071,24 @@ msgstr ""
|
||||
|
||||
msgid "uk"
|
||||
msgstr ""
|
||||
|
||||
msgid "Merge this product with another one"
|
||||
msgstr ""
|
||||
|
||||
msgid "Merge products"
|
||||
msgstr ""
|
||||
|
||||
msgid "Product to keep"
|
||||
msgstr ""
|
||||
|
||||
msgid "Product to remove"
|
||||
msgstr ""
|
||||
|
||||
msgid "Error while merging products"
|
||||
msgstr ""
|
||||
|
||||
msgid "After merging, this product will be kept"
|
||||
msgstr ""
|
||||
|
||||
msgid "After merging, all occurences of this product will be replaced by \"Product to keep\" (means this product will not exist anymore)"
|
||||
msgstr ""
|
||||
|
@ -1,5 +1,8 @@
|
||||
CREATE TRIGGER cascade_product_removal AFTER DELETE ON products
|
||||
BEGIN
|
||||
DELETE FROM stock
|
||||
WHERE product_id = OLD.id;
|
||||
|
||||
DELETE FROM stock_log
|
||||
WHERE product_id = OLD.id;
|
||||
|
||||
|
@ -102,3 +102,29 @@ if (GetUriParam('include_disabled'))
|
||||
{
|
||||
$("#show-disabled").prop('checked', true);
|
||||
}
|
||||
|
||||
|
||||
$(".merge-products-button").on("click", function(e)
|
||||
{
|
||||
var productId = $(e.currentTarget).attr("data-product-id");
|
||||
$("#merge-products-keep").val(productId);
|
||||
$("#merge-products-remove").val("");
|
||||
$("#merge-products-modal").modal("show");
|
||||
});
|
||||
|
||||
$("#merge-products-save-button").on("click", function()
|
||||
{
|
||||
var productIdToKeep = $("#merge-products-keep").val();
|
||||
var productIdToRemove = $("#merge-products-remove").val();
|
||||
|
||||
Grocy.Api.Post("stock/products/" + productIdToKeep.toString() + "/merge/" + productIdToRemove.toString(), {},
|
||||
function(result)
|
||||
{
|
||||
window.location.href = U('/products');
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
Grocy.FrontendHelpers.ShowGenericError('Error while merging products', xhr.response);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -193,6 +193,7 @@ $app->group('/api', function (RouteCollectorProxy $group) {
|
||||
$group->post('/stock/products/{productId}/transfer', '\Grocy\Controllers\StockApiController:TransferProduct');
|
||||
$group->post('/stock/products/{productId}/inventory', '\Grocy\Controllers\StockApiController:InventoryProduct');
|
||||
$group->post('/stock/products/{productId}/open', '\Grocy\Controllers\StockApiController:OpenProduct');
|
||||
$group->post('/stock/products/{productIdToKeep}/merge/{productIdToRemove}', '\Grocy\Controllers\StockApiController:MergeProducts');
|
||||
$group->get('/stock/products/by-barcode/{barcode}', '\Grocy\Controllers\StockApiController:ProductDetailsByBarcode');
|
||||
$group->post('/stock/products/by-barcode/{barcode}/add', '\Grocy\Controllers\StockApiController:AddProductByBarcode');
|
||||
$group->post('/stock/products/by-barcode/{barcode}/consume', '\Grocy\Controllers\StockApiController:ConsumeProductByBarcode');
|
||||
|
@ -1318,6 +1318,53 @@ class StockService extends BaseService
|
||||
}
|
||||
}
|
||||
|
||||
public function MergeProducts(int $productIdToKeep, int $productIdToRemove)
|
||||
{
|
||||
if (!$this->ProductExists($productIdToKeep))
|
||||
{
|
||||
throw new \Exception('$productIdToKeep does not exist or is inactive');
|
||||
}
|
||||
|
||||
if (!$this->ProductExists($productIdToRemove))
|
||||
{
|
||||
throw new \Exception('$productIdToRemove does not exist or is inactive');
|
||||
}
|
||||
|
||||
if ($productIdToKeep == $productIdToRemove)
|
||||
{
|
||||
throw new \Exception('$productIdToKeep cannot equal $productIdToRemove');
|
||||
}
|
||||
|
||||
$this->getDatabaseService()->GetDbConnectionRaw()->beginTransaction();
|
||||
try
|
||||
{
|
||||
$productToKeep = $this->getDatabase()->products($productIdToKeep);
|
||||
$productToRemove = $this->getDatabase()->products($productIdToRemove);
|
||||
$conversion = $this->getDatabase()->quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $productToRemove->id, $productToRemove->qu_id_stock, $productToKeep->qu_id_stock)->fetch();
|
||||
$factor = 1.0;
|
||||
if ($conversion != null)
|
||||
{
|
||||
$factor = floatval($conversion->factor);
|
||||
}
|
||||
|
||||
$this->getDatabaseService()->ExecuteDbStatement('UPDATE stock SET product_id = ' . $productIdToKeep . ', amount = amount * ' . $factor . ' WHERE product_id = ' . $productIdToRemove);
|
||||
$this->getDatabaseService()->ExecuteDbStatement('UPDATE stock_log SET product_id = ' . $productIdToKeep . ', amount = amount * ' . $factor . ' WHERE product_id = ' . $productIdToRemove);
|
||||
$this->getDatabaseService()->ExecuteDbStatement('UPDATE product_barcodes SET product_id = ' . $productIdToKeep . ' WHERE product_id = ' . $productIdToRemove);
|
||||
$this->getDatabaseService()->ExecuteDbStatement('UPDATE quantity_unit_conversions SET product_id = ' . $productIdToKeep . ' WHERE product_id = ' . $productIdToRemove);
|
||||
$this->getDatabaseService()->ExecuteDbStatement('UPDATE recipes_pos SET product_id = ' . $productIdToKeep . ', amount = amount * ' . $factor . ' WHERE product_id = ' . $productIdToRemove);
|
||||
$this->getDatabaseService()->ExecuteDbStatement('UPDATE recipes SET product_id = ' . $productIdToKeep . ' WHERE product_id = ' . $productIdToRemove);
|
||||
$this->getDatabaseService()->ExecuteDbStatement('UPDATE meal_plan SET product_id = ' . $productIdToKeep . ', product_amount = product_amount * ' . $factor . ' WHERE product_id = ' . $productIdToRemove);
|
||||
$this->getDatabaseService()->ExecuteDbStatement('UPDATE shopping_list SET product_id = ' . $productIdToKeep . ', amount = amount * ' . $factor . ' WHERE product_id = ' . $productIdToRemove);
|
||||
$this->getDatabaseService()->ExecuteDbStatement('DELETE FROM products WHERE id = ' . $productIdToRemove);
|
||||
}
|
||||
catch (Exception $ex)
|
||||
{
|
||||
$this->getDatabaseService()->GetDbConnectionRaw()->rollback();
|
||||
throw $ex;
|
||||
}
|
||||
$this->getDatabaseService()->GetDbConnectionRaw()->commit();
|
||||
}
|
||||
|
||||
private function LoadBarcodeLookupPlugin()
|
||||
{
|
||||
$pluginName = defined('GROCY_STOCK_BARCODE_LOOKUP_PLUGIN') ? GROCY_STOCK_BARCODE_LOOKUP_PLUGIN : '';
|
||||
|
@ -136,6 +136,13 @@
|
||||
title="{{ $__t('Copy this item') }}">
|
||||
<i class="fas fa-copy"></i>
|
||||
</a>
|
||||
<a class="btn btn-primary btn-sm merge-products-button"
|
||||
href="#"
|
||||
data-product-id="{{ $product->id }}"
|
||||
data-toggle="tooltip"
|
||||
title="{{ $__t('Merge this product with another one') }}">
|
||||
<i class="fas fa-compress-alt"></i>
|
||||
</a>
|
||||
<a class="btn btn-danger btn-sm product-delete-button"
|
||||
href="#"
|
||||
data-product-id="{{ $product->id }}"
|
||||
@ -193,4 +200,53 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade"
|
||||
id="merge-products-modal"
|
||||
tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content text-center">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title w-100">{{ $__t('Merge products') }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="merge-products-keep">{{ $__t('Product to keep') }} <i class="fas fa-question-circle text-muted"
|
||||
data-toggle="tooltip"
|
||||
title="{{ $__t('After merging, this product will be kept') }}"></i>
|
||||
</label>
|
||||
<select class="custom-control custom-select"
|
||||
id="merge-products-keep">
|
||||
<option></option>
|
||||
@foreach($products as $product)
|
||||
<option value="{{ $product->id }}">{{ $product->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="merge-products-remove">{{ $__t('Product to remove') }} <i class="fas fa-question-circle text-muted"
|
||||
data-toggle="tooltip"
|
||||
title="{{ $__t('After merging, all occurences of this product will be replaced by "Product to keep" (means this product will not exist anymore)') }}"></i>
|
||||
</label>
|
||||
<select class="custom-control custom-select"
|
||||
id="merge-products-remove">
|
||||
<option></option>
|
||||
@foreach($products as $product)
|
||||
<option value="{{ $product->id }}">{{ $product->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button"
|
||||
class="btn btn-secondary"
|
||||
data-dismiss="modal">{{ $__t('Cancel') }}</button>
|
||||
<button id="merge-products-save-button"
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
data-dismiss="modal">{{ $__t('OK') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@stop
|
||||
|
Loading…
x
Reference in New Issue
Block a user