Allow different locations per product in stock (closes #124)

Kind of basic for now, a different location can be set on purchase, the filters on the stock overview page handles different locations
This commit is contained in:
Bernd Bestel 2019-03-01 20:25:01 +01:00
parent 32e878afc9
commit b89643ddb1
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
8 changed files with 63 additions and 10 deletions

View File

@ -66,13 +66,19 @@ class StockApiController extends BaseApiController
$price = $requestBody['price']; $price = $requestBody['price'];
} }
$locationId = null;
if (array_key_exists('location_id', $requestBody) && is_numeric($requestBody['location_id']))
{
$locationId = $requestBody['location_id'];
}
$transactionType = StockService::TRANSACTION_TYPE_PURCHASE; $transactionType = StockService::TRANSACTION_TYPE_PURCHASE;
if (array_key_exists('transaction_type', $requestBody) && !empty($requestBody['transactiontype'])) if (array_key_exists('transaction_type', $requestBody) && !empty($requestBody['transactiontype']))
{ {
$transactionType = $requestBody['transactiontype']; $transactionType = $requestBody['transactiontype'];
} }
$bookingId = $this->StockService->AddProduct($args['productId'], $requestBody['amount'], $bestBeforeDate, $transactionType, date('Y-m-d'), $price); $bookingId = $this->StockService->AddProduct($args['productId'], $requestBody['amount'], $bestBeforeDate, $transactionType, date('Y-m-d'), $price, $locationId);
return $this->ApiResponse(array('booking_id' => $bookingId)); return $this->ApiResponse(array('booking_id' => $bookingId));
} }
catch (\Exception $ex) catch (\Exception $ex)

View File

@ -22,6 +22,7 @@ class StockController extends BaseController
'quantityunits' => $this->Database->quantity_units()->orderBy('name'), 'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
'locations' => $this->Database->locations()->orderBy('name'), 'locations' => $this->Database->locations()->orderBy('name'),
'currentStock' => $this->StockService->GetCurrentStock(), 'currentStock' => $this->StockService->GetCurrentStock(),
'currentStockLocations' => $this->StockService->GetCurrentStockLocations(),
'missingProducts' => $this->StockService->GetMissingProducts(), 'missingProducts' => $this->StockService->GetMissingProducts(),
'nextXDays' => 5, 'nextXDays' => 5,
'productGroups' => $this->Database->product_groups()->orderBy('name') 'productGroups' => $this->Database->product_groups()->orderBy('name')
@ -31,7 +32,8 @@ class StockController extends BaseController
public function Purchase(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) public function Purchase(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{ {
return $this->AppContainer->view->render($response, 'purchase', [ return $this->AppContainer->view->render($response, 'purchase', [
'products' => $this->Database->products()->orderBy('name') 'products' => $this->Database->products()->orderBy('name'),
'locations' => $this->Database->locations()->orderBy('name')
]); ]);
} }

View File

@ -1021,7 +1021,8 @@
"type": "object", "type": "object",
"properties": { "properties": {
"amount": { "amount": {
"type": "double" "type": "number",
"format": "double"
}, },
"best_before_date": { "best_before_date": {
"type": "string", "type": "string",
@ -1035,6 +1036,11 @@
"type": "number", "type": "number",
"format": "double", "format": "double",
"description": "The price per purchase quantity unit in configured currency" "description": "The price per purchase quantity unit in configured currency"
},
"location_id": {
"type": "number",
"format": "integer",
"description": "If omitted, the default location of the product is used"
} }
}, },
"example": { "example": {

15
migrations/0051.sql Normal file
View File

@ -0,0 +1,15 @@
ALTER TABLE stock
ADD location_id INTEGER;
ALTER TABLE stock_log
ADD location_id INTEGER;
CREATE VIEW stock_current_locations
AS
SELECT
s.product_id,
IFNULL(s.location_id, p.location_id) AS location_id
FROM stock s
JOIN products p
ON s.product_id = p.id
GROUP BY s.product_id, IFNULL(s.location_id, p.location_id);

View File

@ -20,6 +20,7 @@
jsonData.amount = amount; jsonData.amount = amount;
jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue(); jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue();
jsonData.price = price; jsonData.price = price;
jsonData.location_id = jsonForm.location_id;
Grocy.Api.Post('stock/products/' + jsonForm.product_id + '/add', jsonData, Grocy.Api.Post('stock/products/' + jsonForm.product_id + '/add', jsonData,
function(result) function(result)
@ -65,6 +66,7 @@
toastr.success(successMessage); toastr.success(successMessage);
$('#amount').val(0); $('#amount').val(0);
$('#price').val(''); $('#price').val('');
$('#location_id').val('');
Grocy.Components.DateTimePicker.Clear(); Grocy.Components.DateTimePicker.Clear();
Grocy.Components.ProductPicker.SetValue(''); Grocy.Components.ProductPicker.SetValue('');
Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.Components.ProductPicker.GetInputElement().focus();
@ -99,6 +101,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
{ {
$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name); $('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name);
$('#price').val(productDetails.last_price); $('#price').val(productDetails.last_price);
$('#location_id').val(productDetails.product.location_id);
if (productDetails.product.allow_partial_units_in_stock == 1) if (productDetails.product.allow_partial_units_in_stock == 1)
{ {

View File

@ -11,18 +11,24 @@ class StockService extends BaseService
public function GetCurrentStock($includeNotInStockButMissingProducts = false) public function GetCurrentStock($includeNotInStockButMissingProducts = false)
{ {
$sql = 'SELECT * from stock_current'; $sql = 'SELECT * FROM stock_current';
if ($includeNotInStockButMissingProducts) if ($includeNotInStockButMissingProducts)
{ {
$sql = 'SELECT * from stock_current WHERE best_before_date IS NOT NULL'; $sql = 'SELECT * FROM stock_current WHERE best_before_date IS NOT NULL';
} }
return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
} }
public function GetCurrentStockLocations()
{
$sql = 'SELECT * FROM stock_current_locations';
return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
}
public function GetMissingProducts() public function GetMissingProducts()
{ {
$sql = 'SELECT * from stock_missing_products'; $sql = 'SELECT * FROM stock_missing_products';
return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
} }
@ -109,7 +115,7 @@ class StockService extends BaseService
} }
} }
public function AddProduct(int $productId, float $amount, string $bestBeforeDate, $transactionType, $purchasedDate, $price) public function AddProduct(int $productId, float $amount, string $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId = null)
{ {
if (!$this->ProductExists($productId)) if (!$this->ProductExists($productId))
{ {
@ -127,7 +133,8 @@ class StockService extends BaseService
'purchased_date' => $purchasedDate, 'purchased_date' => $purchasedDate,
'stock_id' => $stockId, 'stock_id' => $stockId,
'transaction_type' => $transactionType, 'transaction_type' => $transactionType,
'price' => $price 'price' => $price,
'location_id' => $locationId
)); ));
$logRow->save(); $logRow->save();
@ -139,7 +146,8 @@ class StockService extends BaseService
'best_before_date' => $bestBeforeDate, 'best_before_date' => $bestBeforeDate,
'purchased_date' => $purchasedDate, 'purchased_date' => $purchasedDate,
'stock_id' => $stockId, 'stock_id' => $stockId,
'price' => $price 'price' => $price,
'location_id' => $locationId
)); ));
$stockRow->save(); $stockRow->save();

View File

@ -49,6 +49,17 @@
'isRequired' => false 'isRequired' => false
)) ))
<div class="form-group">
<label for="location_id">{{ $L('Location') }}</label>
<select required class="form-control" id="location_id" name="location_id">
<option></option>
@foreach($locations as $location)
<option value="{{ $location->id }}">{{ $location->name }}</option>
@endforeach
</select>
<div class="invalid-feedback">{{ $L('A location is required') }}</div>
</div>
<button id="save-purchase-button" class="btn btn-success">{{ $L('OK') }}</button> <button id="save-purchase-button" class="btn btn-success">{{ $L('OK') }}</button>
</form> </form>

View File

@ -119,7 +119,9 @@
<time id="product-{{ $currentStockEntry->product_id }}-next-best-before-date-timeago" class="timeago timeago-contextual" datetime="{{ $currentStockEntry->best_before_date }} 23:59:59"></time> <time id="product-{{ $currentStockEntry->product_id }}-next-best-before-date-timeago" class="timeago timeago-contextual" datetime="{{ $currentStockEntry->best_before_date }} 23:59:59"></time>
</td> </td>
<td class="d-none"> <td class="d-none">
{{ FindObjectInArrayByPropertyValue($locations, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->location_id)->name }} @foreach(FindAllObjectsInArrayByPropertyValue($currentStockLocations, 'product_id', $currentStockEntry->product_id) as $locationsForProduct)
{{ FindObjectInArrayByPropertyValue($locations, 'id', $locationsForProduct->location_id)->name }}
@endforeach
</td> </td>
<td class="d-none"> <td class="d-none">
@if($currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $currentStockEntry->amount > 0) expired @elseif($currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) expiring @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) belowminstockamount @endif @if($currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $currentStockEntry->amount > 0) expired @elseif($currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) expiring @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) belowminstockamount @endif