Initial asset.inventory service.

This commit is contained in:
Michael Stenta 2021-03-19 12:56:36 -04:00
parent d71612a2b9
commit 4064c4f84f
5 changed files with 332 additions and 0 deletions

View File

@ -39,6 +39,29 @@ a string in Well-Known Text format.
$geometry = \Drupal::service('asset.location')->getGeometry($asset);
```
## Asset inventory service
**Service name**: `asset.inventory`
The asset inventory service provides methods that encapsulate the logic for
determining an asset's inventory.
**Methods**:
`getInventory($asset, $measure = '', $units = '')` - Get inventory summaries
for an asset. Returns an array of arrays with the following keys: `measure`,
`value`, `units`. This can be optionally filtered by `$measure` and `$units`.
**Example usage**:
```php
// Get summaries of all inventories for an asset.
$all_inventory = \Drupal::service('asset.inventory')->getInventory($asset);
// Get the current inventory for a given measure and units.
$gallons_of_fertilizer = \Drupal::service('asset.inventory')->getInventory($asset, 'volume', 'gallons');
```
## Group membership service
**Service name**: `group.membership`

View File

@ -8,3 +8,4 @@ dependencies:
- farm:asset
- farm:farm_field
- farm:quantity
- fraction:fraction

View File

@ -0,0 +1,5 @@
services:
asset.inventory:
class: Drupal\farm_inventory\AssetInventory
arguments:
[ '@datetime.time' ]

View File

@ -0,0 +1,276 @@
<?php
namespace Drupal\farm_inventory;
use Drupal\asset\Entity\AssetInterface;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Database\Database;
use Drupal\fraction\Fraction;
/**
* Asset inventory logic.
*/
class AssetInventory implements AssetInventoryInterface {
/**
* The database object.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The time service.
*
* @var \Drupal\Component\Datetime\TimeInterface
*/
protected $time;
/**
* Class constructor.
*
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
*/
public function __construct(TimeInterface $time) {
$this->database = Database::getConnection();
$this->time = $time;
}
/**
* {@inheritdoc}
*/
public function getInventory(AssetInterface $asset, string $measure = '', int $units = 0): array {
// Get a list of the measure+units pairs we will calculate inventory for.
$measure_units_pairs = $this->getMeasureUnitsPairs($asset, $measure, $units);
// Iterate through the measure+units pairs and build inventory summaries.
$inventories = [];
foreach ($measure_units_pairs as $pair) {
$total = $this->calculateInventory($asset, $pair['measure'], $pair['units']);
$inventories[] = [
'measure' => $pair['measure'] ? $pair['measure'] : '',
'value' => $total->toDecimal(0, TRUE),
'units' => $pair['units'] ? $pair['units'] : NULL,
];
}
// Return the inventory summaries.
return $inventories;
}
/**
* Query the database for all measure+units inventory pairs of an asset.
*
* @param \Drupal\asset\Entity\AssetInterface $asset
* The asset we are querying inventory of.
* @param string $measure
* The quantity measure of the inventory. See quantity_measures().
* @param int $units
* The quantity units of the inventory (term ID).
*
* @return array
* An array of arrays. Each array will have a 'measure' and 'units' key.
*/
protected function getMeasureUnitsPairs(AssetInterface $asset, string $measure = '', int $units = 0) {
// If both a measure and units are provided, that is the only pair.
if (!empty($measure) && !empty($units)) {
return [
[
'measure' => $measure,
'units' => $units,
],
];
}
// Query the database for measure+units pairs.
$query = $this->database->select('quantity', 'q');
$query->condition('q.inventory_asset', $asset->id());
$query->addField('q', 'measure');
$query->addField('q', 'units');
$query->groupBy('q.measure');
$query->groupBy('q.units');
// Filter by measure or units, if provided.
if (!empty($measure)) {
$query->condition('q.measure', $measure);
}
if (!empty($units)) {
$query->condition('q.units', $units);
}
// Execute the query and build the array of measure+units pairs.
$result = $query->execute();
$pairs = [];
foreach ($result as $row) {
$pairs[] = [
'measure' => !empty($row->measure) ? $row->measure : '',
'units' => !empty($row->units) ? $row->units : 0,
];
}
return $pairs;
}
/**
* Query the database for the latest asset "reset" adjustment timestamp.
*
* @param \Drupal\asset\Entity\AssetInterface $asset
* The asset we are querying inventory of.
* @param string $measure
* The quantity measure of the inventory. See quantity_measures().
* @param int $units
* The quantity units of the inventory (term ID).
*
* @return int|null
* Returns a unix timestamp, or NULL if no "reset" adjustment is available.
*/
protected function getLatestResetTimestamp(AssetInterface $asset, string $measure = '', int $units = 0) {
$query = $this->baseQuery($asset, $measure, $units);
$query->condition('q.inventory_adjustment', 'reset');
$query->addExpression('MAX(l.timestamp)');
return $query->execute()->fetchField();
}
/**
* Calculate the inventory of an asset, for a given measure+units pair.
*
* @param \Drupal\asset\Entity\AssetInterface $asset
* The asset we are querying inventory of.
* @param string $measure
* The quantity measure of the inventory. See quantity_measures().
* @param int $units
* The quantity units of the inventory (term ID).
*
* @return \Drupal\fraction\Fraction
* Returns a Fraction object representing the total inventory.
*/
protected function calculateInventory(AssetInterface $asset, string $measure = '', int $units = 0) {
// Query the database for inventory adjustments of the given asset,
// measure, and units.
$adjustments = $this->getAdjustments($asset, $measure, $units);
// Iterate through the results and calculate the inventory.
// This will use fraction math to maintain maximum precision.
$total = new Fraction();
foreach ($adjustments as $adjustment) {
// Create a Fraction object from the numerator and denominator.
$value = new Fraction($adjustment->numerator, $adjustment->denominator);
// Reset/increment/decrement the total.
switch ($adjustment->type) {
// Reset.
case 'reset':
$total = $value;
break;
// Increment.
case 'increment':
$total->add($value);
break;
// Decrement.
case 'decrement':
$total->subtract($value);
break;
}
}
return $total;
}
/**
* Query the database for all inventory adjustments of an asset.
*
* @param \Drupal\asset\Entity\AssetInterface $asset
* The asset we are querying inventory of.
* @param string $measure
* The quantity measure of the inventory. See quantity_measures().
* @param int $units
* The quantity units of the inventory (term ID).
*
* @return array
* An array of objects with the following properties: type (reset,
* increment, or decrement), numerator, and denominator.
*/
protected function getAdjustments(AssetInterface $asset, string $measure = '', int $units = 0) {
// First, query the database to find the timestamp of the most recent
// "reset" adjustment log for this asset (if available).
$latest_reset = $this->getLatestResetTimestamp($asset, $measure, $units);
// Then, query the database for all inventory adjustments.
$query = $this->baseQuery($asset, $measure, $units);
$query->addField('q', 'inventory_adjustment', 'type');
$query->addField('q', 'value__numerator', 'numerator');
$query->addField('q', 'value__denominator', 'denominator');
$query->condition('q.inventory_adjustment', NULL, 'IS NOT NULL');
// Sort by log timestamp and then ID, ascending.
$query->orderBy('l.timestamp', 'ASC');
$query->orderBy('l.id', 'ASC');
// Filter to logs that happened after the the latest reset, if available.
if (!empty($latest_reset)) {
$query->condition('l.timestamp', $latest_reset, '>=');
}
// Execute the query and return the results.
return $query->execute()->fetchAll();
}
/**
* Build a base query for getting asset inventory adjustments.
*
* @param \Drupal\asset\Entity\AssetInterface $asset
* The asset we are querying inventory of.
* @param string $measure
* The quantity measure of the inventory. See quantity_measures().
* @param int $units
* The quantity units of the inventory (term ID).
*
* @return \Drupal\Core\Database\Query\SelectInterface
* A database query object.
*/
protected function baseQuery(AssetInterface $asset, string $measure = '', int $units = 0) {
// Start with a query of the quantity base table.
$query = $this->database->select('quantity', 'q');
// Only include adjustments that reference the asset.
$query->condition('q.inventory_asset', $asset->id());
// Filter by measure and units. If either is empty, then explicitly filter
// to only include rows with NULL values.
if (!empty($measure)) {
$query->condition('q.measure', $measure);
}
else {
$query->condition('q.measure', NULL, 'IS NULL');
}
if (!empty($units)) {
$query->condition('q.units', $units);
}
else {
$query->condition('q.units', NULL, 'IS NULL');
}
// Join the {log_field_data} table (via reverse reference through
// the {log__quantity} table).
$query->join('log__quantity', 'lq', 'q.id = lq.quantity_target_id');
$query->join('log_field_data', 'l', 'lq.entity_id = l.id');
// Filter out logs that are not done.
$query->condition('l.status', 'done');
// Filter out future logs.
$query->condition('l.timestamp', $this->time->getRequestTime(), '<=');
// Return the query.
return $query;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Drupal\farm_inventory;
use Drupal\asset\Entity\AssetInterface;
/**
* Asset inventory logic.
*/
interface AssetInventoryInterface {
/**
* Get inventory summaries for an asset.
*
* @param \Drupal\asset\Entity\AssetInterface $asset
* The Asset entity.
* @param string $measure
* The quantity measure of the inventory. See quantity_measures().
* @param int $units
* The quantity units of the inventory (term ID).
*
* @return array
* Returns an array of asset inventory information.
*/
public function getInventory(AssetInterface $asset, string $measure = '', int $units = 0): array;
}