Add form to upload kroger receipt data

Add a new menu option to "Upload JSON" that will accept Kroger grocery
store json receipt data.
This commit is contained in:
Zeb Fross 2020-04-01 19:33:57 -07:00
parent 53f1321e19
commit aa91418364
10 changed files with 323 additions and 4 deletions

View File

@ -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

View File

@ -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)
{

View File

@ -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)
{

View File

@ -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 ""

View File

@ -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)
}
);
});

View File

@ -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"],

View File

@ -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)
{

View File

@ -1,4 +1,4 @@
{
"Version": "2.6.2",
"Version": "2.6.3",
"ReleaseDate": "2020-03-29"
}

View File

@ -206,6 +206,14 @@
</a>
</li>
@endif
@if(GROCY_FEATURE_FLAG_UPLOAD_JSON)
<li class="nav-item nav-item-sidebar" data-toggle="tooltip" data-placement="right" title="{{ $__t('Upload json') }}" data-nav-for-page="upload-json">
<a class="nav-link discrete-link" href="{{ $U('/uploadjson') }}">
<i class="fas fa-fire"></i>
<span class="nav-link-text">{{ $__t('Upload Json') }}</span>
</a>
</li>
@endif
@php $firstUserentity = true; @endphp
@foreach($userentitiesForSidebar as $userentity)

View File

@ -0,0 +1,71 @@
@extends('layout.default')
@section('title', $__t('Upload Purchase Data As JSON'))
@section('activeNav', 'uploadjson')
@section('viewJsName', 'uploadjson')
@section('content')
<div class="row">
<div class="col-md-12">
<h1>
@yield('title')
</h1>
</div>
</div>
<div class="row">
<div class="col-lg-6 col-xs-12">
<form id="json-form" novalidate>
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
<div class="form-group">
<label for="location_id">{{ $__t('Default location ') . $location->name }}</label>
<p class="small text-muted">{{ $__t('Change this value in user settings') }}</p>
</div>
@endif
<div class="form-group">
<label for="qu_id_purchase">{{ $__t('Default Quantity unit ') . $quantityunit->name }}</label>
<p class="small text-muted">{{ $__t('Change this value in user settings') }}</p>
</div>
<div class="form-group">
<label for="shopping_location_id">{{ $__t('Default store') }}</label>
<select class="form-control input-group-qu" id="shopping_location_id" name="shopping_location_id">
<option></option>
@foreach($shoppinglocations as $location)
<option value="{{ $location->id }}">{{ $location->name }}</option>
@endforeach
</select>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="dont_add_to_stock" name="dont_add_to_stock" value="1">
<label class="form-check-label" for="dont_add_to_stock">{{ $__t("Don't add to stock") }}</label>
</div>
</div>
<div class="form-group">
<label for="json-data">{{ $__t('JSON Data') }}</label>
<textarea class="form-control" rows="75" id="json-data" name="json-data" placeholder="{'data': [{'items': [{}]}] }"></textarea>
</div>
<button id="upload-json-button" class="btn btn-success d-block">{{ $__t('OK') }}</button>
</form>
</div>
<div class="col-lg-6 col-xs-12">
<p>
{{ $__t("Kroger grocery stores allow manual access to past purchases in json form
through a browser's developer tools.") }}
</p>
<p>
{{ $__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.") }}
</p>
</div>
</div>
@stop