Grocycode: Productpicker, StockService

This commit is contained in:
Katharina Bogad 2021-06-06 22:05:29 +02:00
parent 4a4d9c451f
commit 8a36b09485
5 changed files with 205 additions and 10 deletions

18
.vscode/settings.json vendored
View File

@ -14,4 +14,20 @@
"php-cs-fixer.formatHtml": true,
"php-cs-fixer.autoFixBySemicolon": true,
"php-cs-fixer.onsave": true,
}
"phpfmt.passes": [
"PSR2KeywordsLowerCase",
"PSR2LnAfterNamespace",
"PSR2CurlyOpenNextLine",
"PSR2ModifierVisibilityStaticOrder",
"PSR2SingleEmptyLineAndStripClosingTag",
"ReindentSwitchBlocks",
"AllmanStyleBraces",
"StripExtraCommaInArray"
],
"phpfmt.exclude": [
"ReindentComments",
"StripNewlineWithinClassBody"
],
"phpfmt.psr2": false,
"phpfmt.indent_with_space": false,
}

142
helpers/Grocycode.php Normal file
View File

@ -0,0 +1,142 @@
<?php
namespace Grocy\Helpers;
/**
* A class that abstracts grocycode.
*
* grocycode is a simple, easily serializable format to reference
* stuff within grocy. It consists of n (n 3) double-colon seperated parts:
*
* 1. The magic `grcy`
* 2. A type identifer, must match `[a-z]+` (i.e. only lowercase ascii, minimum length 1 character)
* 3. An object id
* 4. Any number of further data fields, double-colon seperated.
*
* @author Katharina Bogad <katharina@hacked.xyz>
*/
class Grocycode
{
public const PRODUCT = "p";
public const BATTERY = "b";
public const CHORE = "c";
public const MAGIC = "grcy";
/**
* An array that registers all valid grocycode types. Register yours here by appending to this array.
*/
public static $Items = [self::PRODUCT, self::BATTERY, self::CHORE];
private $type, $id, $extra_data = [];
/**
* Validates a grocycode.
*
* Returns true, if a supplied $code is a valid grocycode, false otherwise.
*
* @return bool
*/
public static function Validate(string $code)
{
try
{
$gc = new self($code);
return true;
}
catch (Exception $e)
{
return false;
}
}
/**
* Constructs a new instance of the Grocycode class.
*
* Because php doesn't support overloading, this is a proxy
* to either setFromCode($code) or setFromData($type, $id, $extra_data = []).
*/
public function __construct(...$args)
{
$argc = count($args);
if ($argc == 1)
{
$this->setFromCode($args[0]);
return;
}
else if ($argc == 2 || $argc == 3)
{
if ($argc == 2)
{
$args[] = [];
}
$this->setFromData($args[0], $args[1], $args[2]);
return;
}
throw new \Exception("No suitable overload found.");
}
/**
* Parses a grocycode.
*/
private function setFromCode($code)
{
$parts = array_reverse(explode(":", $barcode));
if (array_pop($parts) != self::MAGIC)
{
throw new \Exception("Not a grocycode");
}
if (!in_array($this->type = array_pop($parts), self::$Items))
{
throw new \Exception("Unknown grocycode type");
}
$this->id = array_pop($parts);
$this->extra_data = array_reverse($parse);
}
/**
* Constructs a grocycode from data.
*/
private function setFromData($type, $id, $extra_data = [])
{
if (!is_array($extra_data))
{
throw new \Exception("Extra data must be array of string");
}
if (!in_array($type, self::$Items))
{
throw new \Exception("Unknown grocycode type");
}
$this->type = $type;
$this->id = $id;
$this->extra_data = $extra_data;
}
public function GetId()
{
return $this->id;
}
public function GetExtraData()
{
return $this->extra_data;
}
public function GetType()
{
return $this->type;
}
public function __toString(): string
{
$arr = array_merge([self::MAGIC, $this->type, $this->id], $this->extra_data);
return implode(":", $arr);
}
}

View File

@ -147,7 +147,23 @@ $('#product_id_text_input').on('blur', function(e)
$('#product_id').attr("barcode", "null");
var input = $('#product_id_text_input').val().toString();
var possibleOptionElement = $("#product_id option[data-additional-searchdata*=\"" + input + ",\"]").first();
var possibleOptionElement = [];
// did we enter a grocycode?
if (input.startsWith("grcy"))
{
var gc = input.split(":");
if (gc[1] == "p")
{
// find product id
possibleOptionElement = $("#product_id option[value=\"" + gc[2] + "\"]").first();
$("#product_id").data("grocycode", true);
}
}
else // process barcode as usual
{
possibleOptionElement = $("#product_id option[data-additional-searchdata*=\"" + input + ",\"]").first();
}
if (GetUriParam('flow') === undefined && input.length > 0 && possibleOptionElement.length > 0)
{

View File

@ -223,6 +223,18 @@ $("#location_id").on('change', function(e)
{
stockId = GetUriParam('stockId');
}
else
{
// try to get stock id from grocycode
if ($("#product_id").data("grocycode"))
{
var gc = $("#product_id").attr("barcode").split(":");
if (gc.length == 4)
{
stockId = gc[3];
}
}
}
if (locationId)
{
@ -249,6 +261,7 @@ $("#location_id").on('change', function(e)
if (stockEntry.stock_id == stockId)
{
$("#use_specific_stock_entry").click();
$("#specific_stock_entry").val(stockId);
}
}

View File

@ -1,7 +1,8 @@
<?php
namespace Grocy\Services;
use Grocy\Helpers\Grocycode;
class StockService extends BaseService
{
const TRANSACTION_TYPE_CONSUME = 'consume';
@ -33,7 +34,7 @@ class StockService extends BaseService
if ($alreadyExistingEntry)
{ // Update
if ($alreadyExistingEntry->amount < $amountToAdd)
{
{
$alreadyExistingEntry->update([
'amount' => $amountToAdd,
'shopping_list_id' => $listId
@ -173,7 +174,7 @@ class StockService extends BaseService
'stock_id' => $stockId,
'price' => $price,
'location_id' => $locationId,
'shopping_location_id' => $shoppingLocationId,
'shopping_location_id' => $shoppingLocationId
]);
$stockRow->save();
@ -240,7 +241,7 @@ class StockService extends BaseService
throw new \Exception('Location does not exist');
}
$productDetails = (object)$this->GetProductDetails($productId);
$productDetails = (object) $this->GetProductDetails($productId);
// Tare weight handling
// The given amount is the new total amount including the container weight (gross)
@ -280,7 +281,7 @@ class StockService extends BaseService
// TODO: This check doesn't really check against products only at the given location
// (as GetProductDetails returns the stock_amount_aggregated of all locations)
// However, $potentialStockEntries are filtered accordingly, so this currently isn't really a problem at the end
$productStockAmount = ((object)$this->GetProductDetails($productId))->stock_amount_aggregated;
$productStockAmount = ((object) $this->GetProductDetails($productId))->stock_amount_aggregated;
if ($amount > $productStockAmount)
{
throw new \Exception('Amount to be consumed cannot be > current stock amount (if supplied, at the desired location)');
@ -451,7 +452,7 @@ class StockService extends BaseService
if ($pluginOutput !== null)
{ // Lookup was successful
if ($addFoundProduct === true)
{
{
// Add product to database and include new product id in output
$newRow = $this->getDatabase()->products()->createRow($pluginOutput);
$newRow->save();
@ -640,6 +641,13 @@ class StockService extends BaseService
public function GetProductIdFromBarcode(string $barcode)
{
// first, try to parse this as a product grocycode
if (Grocycode::Validate($barcode))
{
$gc = new Grocycode($barcode);
return intval($gc->GetId());
}
$potentialProduct = $this->getDatabase()->product_barcodes()->where('barcode = :1', $barcode)->fetch();
if ($potentialProduct === null)
@ -728,7 +736,7 @@ class StockService extends BaseService
throw new \Exception('Product does not exist or is inactive');
}
$productDetails = (object)$this->GetProductDetails($productId);
$productDetails = (object) $this->GetProductDetails($productId);
if ($price === null)
{
@ -787,7 +795,7 @@ class StockService extends BaseService
throw new \Exception('Product does not exist or is inactive');
}
$productDetails = (object)$this->GetProductDetails($productId);
$productDetails = (object) $this->GetProductDetails($productId);
$productStockAmountUnopened = floatval($productDetails->stock_amount_aggregated) - floatval($productDetails->stock_amount_opened_aggregated);
$potentialStockEntries = $this->GetProductStockEntries($productId, true, $allowSubproductSubstitution);
$product = $this->getDatabase()->products($productId);