From aa914183645e9a215c5646fd05e91840c6fe172a Mon Sep 17 00:00:00 2001 From: Zeb Fross Date: Wed, 1 Apr 2020 19:33:57 -0700 Subject: [PATCH] Add form to upload kroger receipt data Add a new menu option to "Upload JSON" that will accept Kroger grocery store json receipt data. --- config-dist.php | 1 + controllers/StockApiController.php | 38 +++++++++++++ controllers/StockController.php | 23 ++++++++ localization/strings.pot | 54 +++++++++++++++++- public/viewjs/uploadjson.js | 27 +++++++++ routes.php | 12 ++++ services/StockService.php | 91 +++++++++++++++++++++++++++++- version.json | 2 +- views/layout/default.blade.php | 8 +++ views/uploadjson.blade.php | 71 +++++++++++++++++++++++ 10 files changed, 323 insertions(+), 4 deletions(-) create mode 100644 public/viewjs/uploadjson.js create mode 100644 views/uploadjson.blade.php diff --git a/config-dist.php b/config-dist.php index 82ae183e..472f946c 100644 --- a/config-dist.php +++ b/config-dist.php @@ -139,6 +139,7 @@ Setting('FEATURE_FLAG_TASKS', true); Setting('FEATURE_FLAG_BATTERIES', true); Setting('FEATURE_FLAG_EQUIPMENT', true); Setting('FEATURE_FLAG_CALENDAR', true); +Setting('FEATURE_FLAG_UPLOAD_JSON', true); # Sub feature flags diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index 5db2038b..8f6762e8 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -289,6 +289,44 @@ class StockApiController extends BaseApiController return $this->GenericErrorResponse($response, $ex->getMessage()); } } + + public function UploadJson(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) + { + $requestBody = $request->getParsedBody(); + + try + { + if ($requestBody === null) + { + throw new \Exception('Request body could not be parsed (probably invalid JSON format or missing/wrong Content-Type header)'); + } + + if (!array_key_exists('json-data', $requestBody)) + { + throw new \Exception('JSON data is required'); + } + + $default_location_id = $this->getUsersService()->GetUserSetting(GROCY_USER_ID, 'product_presets_location_id'); + $default_qu_id = $this->getUsersService()->GetUserSetting(GROCY_USER_ID, 'product_presets_qu_id'); + + if (!$default_location_id) + $default_location_id = $this->getDatabase()->locations()->limit(1)->fetch()['id']; + + if (!$default_qu_id) + $default_qu_id = $this->getDatabase()->quantity_units()->limit(1)->fetch()['id']; + + $shopping_location_id = array_key_exists('shopping_location_id', $requestBody) ? $requestBody['shopping_location_id'] : null; + $parsedData = json_decode($requestBody['json-data'], true); + + $lastInsertId = $this->getStockService()->AddMultipleProducts($parsedData, $default_qu_id, + $default_location_id, $requestBody['dont_add_to_stock'] == "1", $shopping_location_id); + return $this->ApiResponse($response, $lastInsertId); + } + catch (\Exception $ex) + { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } public function InventoryProduct(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { diff --git a/controllers/StockController.php b/controllers/StockController.php index f42546e6..ede428e7 100644 --- a/controllers/StockController.php +++ b/controllers/StockController.php @@ -73,6 +73,29 @@ class StockController extends BaseController 'locations' => $this->getDatabase()->locations()->orderBy('name') ]); } + + public function UploadJson(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) + { + $location_id = $this->getUsersService()->GetUserSetting(GROCY_USER_ID, 'product_presets_location_id'); + $location = null; + if ($location_id > 0) + $location = $this->getDatabase()->locations()->where('id', $location_id).fetch(); + else + $location = $this->getDatabase()->locations()->limit(1)->fetch(); + + $qu_id = $this->getUsersService()->GetUserSetting(GROCY_USER_ID, 'product_presets_location_id'); + $quantity_unit = null; + if ($qu_id > 0) + $quantity_unit = $this->getDatabase()->quantity_units()->where('id', $qui_id).fetch(); + else + $quantity_unit = $this->getDatabase()->quantity_units()->limit(1)->fetch(); + + return $this->renderPage($response, 'uploadjson', [ + 'location' => $location, + 'quantityunit' => $quantity_unit, + 'shoppinglocations' => $this->getDatabase()->shopping_locations()->orderBy('name') + ]); + } public function Inventory(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { diff --git a/localization/strings.pot b/localization/strings.pot index bcb38911..4addd887 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1760,14 +1760,66 @@ msgstr "" msgid "Group ingredients by their product group" msgstr "" -msgid "Unknown store" +msgid "Upload json" +msgstr "" + +msgid "Upload Json" +msgstr "" + +msgid "Upload Purchase Data As JSON" +msgstr "" + +msgid "Error while saving, probably an error in the json" +msgstr "" + +msgid "Default Quantity unit purchase" +msgstr "" + +msgid "Don't add old purchases to stock" +msgstr "" + +msgid "JSON Data" +msgstr "" + +msgid "" +"Kroger grocery stores allow manual access to past purchases in json form \r\n" +"\t\t\tthrough a browser's developer tools." +msgstr "" + +msgid "" +"To get this json, open developer tools\r\n" +"\t\t\tand navigate to https://www.qfc.com/mypurchases (or another Kroger grocer,\r\n" +"\t\t\tsuch as https://www.fredmeyer.com/mypurchases) and look for a call to \r\n" +"\t\t\t/mypurchases/api/v1/receipt/details. The response will contain data for the\r\n" +"\t\t\tlast five receipts." msgstr "" msgid "Store" msgstr "" +msgid "Default shopping location" +msgstr "" + msgid "Transaction successfully undone" msgstr "" +msgid "Unknown store" +msgstr "" + msgid "Default store" msgstr "" + +msgid "Default location " +msgstr "" + +msgid "Change this value in user settings" +msgstr "" + +msgid "Default Quantity unit " +msgstr "" + +msgid "Don't add to stock" +msgstr "" + +msgid "" +msgstr "" diff --git a/public/viewjs/uploadjson.js b/public/viewjs/uploadjson.js new file mode 100644 index 00000000..ca395126 --- /dev/null +++ b/public/viewjs/uploadjson.js @@ -0,0 +1,27 @@ +$('#upload-json-button').on('click', function(e) +{ + e.preventDefault(); + + var redirectDestination = U('/stockoverview'); + var returnTo = GetUriParam('returnto'); + if (returnTo !== undefined) + { + redirectDestination = U(returnTo); + } + + var jsonData = $('#json-form').serializeJSON({ checkboxUncheckedValue: "0" }); + + Grocy.FrontendHelpers.BeginUiBusy("json-form"); + + Grocy.Api.Post('uploadjson', jsonData, + function(result) + { + window.location.href = redirectDestination + }, + function (xhr) + { + Grocy.FrontendHelpers.EndUiBusy("json-form"); + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably an error in the json', xhr.response) + } + ); +}); \ No newline at end of file diff --git a/routes.php b/routes.php index 8df89867..810eb3c6 100644 --- a/routes.php +++ b/routes.php @@ -128,6 +128,12 @@ $app->group('', function(RouteCollectorProxy $group) $group->get('/calendar', '\Grocy\Controllers\CalendarController:Overview'); } + // Upload json routes + if (GROCY_FEATURE_FLAG_UPLOAD_JSON) + { + $group->get('/uploadjson', '\Grocy\Controllers\StockController:UploadJson'); + } + // OpenAPI routes $group->get('/api', '\Grocy\Controllers\OpenApiController:DocumentationUi'); $group->get('/manageapikeys', '\Grocy\Controllers\OpenApiController:ApiKeysList'); @@ -249,6 +255,12 @@ $app->group('/api', function(RouteCollectorProxy $group) $group->get('/calendar/ical', '\Grocy\Controllers\CalendarApiController:Ical')->setName('calendar-ical'); $group->get('/calendar/ical/sharing-link', '\Grocy\Controllers\CalendarApiController:IcalSharingLink'); } + + // Upload json routes + if (GROCY_FEATURE_FLAG_UPLOAD_JSON) + { + $group->post('/uploadjson', '\Grocy\Controllers\StockApiController:UploadJson'); + } })->add(new CorsMiddleware([ 'origin' => ["*"], 'methods' => ["GET", "POST"], diff --git a/services/StockService.php b/services/StockService.php index 2b794bf2..14e01005 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -2,6 +2,8 @@ namespace Grocy\Services; +use Grocy\Helpers\KrogerToGrocyConverter; + class StockService extends BaseService { const TRANSACTION_TYPE_PURCHASE = 'purchase'; @@ -72,8 +74,8 @@ class StockService extends BaseService { return $this->getDatabase()->stock_current_locations()->where('product_id', $productId)->fetchAll(); } - - public function GetProductIdFromBarcode(string $barcode) + + public function GetProductFromBarcode(string $barcode) { $potentialProduct = $this->getDatabase()->products()->where("',' || barcode || ',' LIKE '%,' || :1 || ',%' AND IFNULL(barcode, '') != ''", $barcode)->limit(1)->fetch(); @@ -82,6 +84,25 @@ class StockService extends BaseService throw new \Exception("No product with barcode $barcode found"); } + return $potentialProduct; + } + + public function GetProductFromName(string $name) + { + $potentialProduct = $this->getDatabase()->products()->where("',' || name || ',' LIKE '%,' || :1 || ',%'", $name)->limit(1)->fetch(); + + if ($potentialProduct === null) + { + throw new \Exception("No product with name $name found"); + } + + return $potentialProduct; + } + + public function GetProductIdFromBarcode(string $barcode) + { + $potentialProduct = $this->GetProductFromBarcode($barcode); + return intval($potentialProduct->id); } @@ -598,6 +619,72 @@ class StockService extends BaseService return $this->getDatabase()->lastInsertId(); } + public function AddMultipleProducts($data, $defaultQuantityUnits, $defaultLocation, $dontAddToStock, $shoppingLocation) + { + $products = KrogerToGrocyConverter::ConvertJson($data, $defaultQuantityUnits, $defaultLocation); + + foreach ($products as &$product) + { + $existingProduct = null; + try + { + $existingProduct = $this->GetProductFromBarcode($product["barcode"]); + } + catch (\Exception $ex) + { + try + { + $existingProduct = $this->GetProductFromName($product["name"]); + } + catch (\Exception $ex) + { + if (defined(GROCY_STOCK_BARCODE_LOOKUP_PLUGIN) && GROCY_STOCK_BARCODE_LOOKUP_PLUGIN != "DemoBarcodeLookupPlugin") + { + $existingProduct = $this->ExternalBarcodeLookup($product["barcode"], true /*addFoundProduct*/); + } + + if ($existingProduct == null) + { + $existingProduct = array( + 'name' => $product['name'], + 'location_id' => $product['location_id'], + 'qu_id_purchase' => $product['qu_id_purchase'], + 'qu_id_stock' => $product['qu_id_stock'], + 'qu_factor_purchase_to_stock' => $product['qu_factor_purchase_to_stock'], + 'default_best_before_days' => $product['default_best_before_days'], + 'barcode' => $product['barcode'], + 'picture_url' => $product['picture_url'], + 'shopping_location_id' => $shoppingLocation, + 'cumulate_min_stock_amount_of_sub_products' => 1, + 'min_stock_amount' => 1 + ); + $newRow = $this->getDatabase()->products()->createRow($existingProduct); + $newRow->save(); + + $existingProduct['id'] = $newRow->id; + } + } + } + + $bestBeforeDays = -1; + if ($existingProduct['default_best_before_days'] > -1) + { + $bestBeforeDays = $existingProduct['default_best_before_days']; + } + + $this->AddProduct($existingProduct['id'], $product["quantity"], null /*newBestBeforeDate*/, StockService::TRANSACTION_TYPE_PURCHASE, + $product["transaction_date"], $product["price_paid"], $existingProduct['location_id'], $existingProduct['shopping_location_id']); + + + if ($dontAddToStock) + { + $this->ConsumeProduct($existingProduct['id'], $product['quantity'], false /*spoiled*/, self::TRANSACTION_TYPE_INVENTORY_CORRECTION); + } + } + + return $this->getDatabase()->lastInsertId(); + } + public function EditStockEntry(int $stockRowId, int $amount, $bestBeforeDate, $locationId, $shoppingLocationId, $price, $open, $purchasedDate) { diff --git a/version.json b/version.json index 1286133c..1f57e4d1 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { - "Version": "2.6.2", + "Version": "2.6.3", "ReleaseDate": "2020-03-29" } diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index 0ac57062..89d0860b 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -206,6 +206,14 @@ @endif + @if(GROCY_FEATURE_FLAG_UPLOAD_JSON) + + @endif @php $firstUserentity = true; @endphp @foreach($userentitiesForSidebar as $userentity) diff --git a/views/uploadjson.blade.php b/views/uploadjson.blade.php new file mode 100644 index 00000000..fe5a21e1 --- /dev/null +++ b/views/uploadjson.blade.php @@ -0,0 +1,71 @@ +@extends('layout.default') + +@section('title', $__t('Upload Purchase Data As JSON')) +@section('activeNav', 'uploadjson') +@section('viewJsName', 'uploadjson') + +@section('content') +
+
+

+ @yield('title') +

+
+
+
+
+
+ + @if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) +
+ +

{{ $__t('Change this value in user settings') }}

+
+ @endif + +
+ +

{{ $__t('Change this value in user settings') }}

+
+ +
+ + +
+ +
+
+ + +
+
+ +
+ + +
+ + + +
+
+
+

+ {{ $__t("Kroger grocery stores allow manual access to past purchases in json form + through a browser's developer tools.") }} +

+

+ {{ $__t("To get this json, open developer tools + and navigate to https://www.qfc.com/mypurchases (or another Kroger grocer, + such as https://www.fredmeyer.com/mypurchases) and look for a call to + /mypurchases/api/v1/receipt/details. The response will contain data for the + last five receipts.") }} +

+
+
+@stop