mirror of
				https://github.com/grocy/grocy.git
				synced 2025-11-03 20:15:04 +00:00 
			
		
		
		
	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:
		@@ -66,13 +66,19 @@ class StockApiController extends BaseApiController
 | 
			
		||||
				$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;
 | 
			
		||||
			if (array_key_exists('transaction_type', $requestBody)  && !empty($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));
 | 
			
		||||
		}
 | 
			
		||||
		catch (\Exception $ex)
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ class StockController extends BaseController
 | 
			
		||||
			'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
 | 
			
		||||
			'locations' => $this->Database->locations()->orderBy('name'),
 | 
			
		||||
			'currentStock' => $this->StockService->GetCurrentStock(),
 | 
			
		||||
			'currentStockLocations' => $this->StockService->GetCurrentStockLocations(),
 | 
			
		||||
			'missingProducts' => $this->StockService->GetMissingProducts(),
 | 
			
		||||
			'nextXDays' => 5,
 | 
			
		||||
			'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)
 | 
			
		||||
	{
 | 
			
		||||
		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')
 | 
			
		||||
		]);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1021,7 +1021,8 @@
 | 
			
		||||
								"type": "object",
 | 
			
		||||
								"properties": {
 | 
			
		||||
									"amount": {
 | 
			
		||||
										"type": "double"
 | 
			
		||||
										"type": "number",
 | 
			
		||||
										"format": "double"
 | 
			
		||||
									},
 | 
			
		||||
									"best_before_date": {
 | 
			
		||||
										"type": "string",
 | 
			
		||||
@@ -1035,6 +1036,11 @@
 | 
			
		||||
										"type": "number",
 | 
			
		||||
										"format": "double",
 | 
			
		||||
										"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": {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								migrations/0051.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								migrations/0051.sql
									
									
									
									
									
										Normal 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);
 | 
			
		||||
@@ -20,6 +20,7 @@
 | 
			
		||||
			jsonData.amount = amount;
 | 
			
		||||
			jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue();
 | 
			
		||||
			jsonData.price = price;
 | 
			
		||||
			jsonData.location_id = jsonForm.location_id;
 | 
			
		||||
 | 
			
		||||
			Grocy.Api.Post('stock/products/' + jsonForm.product_id + '/add', jsonData,
 | 
			
		||||
				function(result)
 | 
			
		||||
@@ -65,6 +66,7 @@
 | 
			
		||||
						toastr.success(successMessage);
 | 
			
		||||
						$('#amount').val(0);
 | 
			
		||||
						$('#price').val('');
 | 
			
		||||
						$('#location_id').val('');
 | 
			
		||||
						Grocy.Components.DateTimePicker.Clear();
 | 
			
		||||
						Grocy.Components.ProductPicker.SetValue('');
 | 
			
		||||
						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);
 | 
			
		||||
				$('#price').val(productDetails.last_price);
 | 
			
		||||
				$('#location_id').val(productDetails.product.location_id);
 | 
			
		||||
 | 
			
		||||
				if (productDetails.product.allow_partial_units_in_stock == 1)
 | 
			
		||||
				{
 | 
			
		||||
 
 | 
			
		||||
@@ -11,18 +11,24 @@ class StockService extends BaseService
 | 
			
		||||
 | 
			
		||||
	public function GetCurrentStock($includeNotInStockButMissingProducts = false)
 | 
			
		||||
	{
 | 
			
		||||
		$sql = 'SELECT * from stock_current';
 | 
			
		||||
		$sql = 'SELECT * FROM stock_current';
 | 
			
		||||
		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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function GetCurrentStockLocations()
 | 
			
		||||
	{
 | 
			
		||||
		$sql = 'SELECT * FROM stock_current_locations';
 | 
			
		||||
		return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function GetMissingProducts()
 | 
			
		||||
	{
 | 
			
		||||
		$sql = 'SELECT * from stock_missing_products';
 | 
			
		||||
		$sql = 'SELECT * FROM stock_missing_products';
 | 
			
		||||
		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))
 | 
			
		||||
		{
 | 
			
		||||
@@ -127,7 +133,8 @@ class StockService extends BaseService
 | 
			
		||||
				'purchased_date' => $purchasedDate,
 | 
			
		||||
				'stock_id' => $stockId,
 | 
			
		||||
				'transaction_type' => $transactionType,
 | 
			
		||||
				'price' => $price
 | 
			
		||||
				'price' => $price,
 | 
			
		||||
				'location_id' => $locationId
 | 
			
		||||
			));
 | 
			
		||||
			$logRow->save();
 | 
			
		||||
 | 
			
		||||
@@ -139,7 +146,8 @@ class StockService extends BaseService
 | 
			
		||||
				'best_before_date' => $bestBeforeDate,
 | 
			
		||||
				'purchased_date' => $purchasedDate,
 | 
			
		||||
				'stock_id' => $stockId,
 | 
			
		||||
				'price' => $price
 | 
			
		||||
				'price' => $price,
 | 
			
		||||
				'location_id' => $locationId
 | 
			
		||||
			));
 | 
			
		||||
			$stockRow->save();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -49,6 +49,17 @@
 | 
			
		||||
				'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>
 | 
			
		||||
 | 
			
		||||
		</form>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
					</td>
 | 
			
		||||
					<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 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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user