mirror of
https://github.com/grocy/grocy.git
synced 2026-04-05 20:36:15 +02:00
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:
parent
53f1321e19
commit
aa91418364
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
27
public/viewjs/uploadjson.js
Normal file
27
public/viewjs/uploadjson.js
Normal 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)
|
||||
}
|
||||
);
|
||||
});
|
||||
12
routes.php
12
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"],
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"Version": "2.6.2",
|
||||
"Version": "2.6.3",
|
||||
"ReleaseDate": "2020-03-29"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
71
views/uploadjson.blade.php
Normal file
71
views/uploadjson.blade.php
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user