clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
name: Birth Quick Form
description: Provides a quick form for recording births.
type: module
package: farmOS Quick Forms
core_version_requirement: ^10
dependencies:
- farm:farm_animal
- farm:farm_birth
- farm:farm_observation
- farm:farm_quantity_standard
- farm:farm_quick

View File

@@ -0,0 +1,525 @@
<?php
namespace Drupal\farm_quick_birth\Plugin\QuickForm;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\Session\AccountInterface;
use Drupal\farm_group\GroupMembershipInterface;
use Drupal\farm_location\AssetLocationInterface;
use Drupal\farm_quick\Plugin\QuickForm\QuickFormBase;
use Drupal\farm_quick\Traits\QuickAssetTrait;
use Drupal\farm_quick\Traits\QuickLogTrait;
use Drupal\farm_quick\Traits\QuickStringTrait;
use Psr\Container\ContainerInterface;
/**
* Birth quick form.
*
* @QuickForm(
* id = "birth",
* label = @Translation("Birth"),
* description = @Translation("Record an animal birth."),
* helpText = @Translation("Use this form to record the birth of one or more animals. A new birth log will be created, along with the new child animal asset records."),
* permissions = {
* "create animal asset",
* "create birth log",
* "create observation log",
* }
* )
*/
class Birth extends QuickFormBase {
use QuickAssetTrait;
use QuickLogTrait;
use QuickStringTrait;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Asset location service.
*
* @var \Drupal\farm_location\AssetLocationInterface
*/
protected $assetLocation;
/**
* Current user object.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Group membership service.
*
* @var \Drupal\farm_group\GroupMembershipInterface|null
*/
protected $groupMembership = NULL;
/**
* Constructs a QuickFormBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\farm_location\AssetLocationInterface $asset_location
* Asset location service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* Current user object.
* @param \Drupal\farm_group\GroupMembershipInterface|null $group_membership
* Group membership service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MessengerInterface $messenger, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory, AssetLocationInterface $asset_location, AccountInterface $current_user, ?GroupMembershipInterface $group_membership = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $messenger);
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
$this->configFactory = $config_factory;
$this->assetLocation = $asset_location;
$this->currentUser = $current_user;
$this->groupMembership = $group_membership;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('messenger'),
$container->get('entity_type.manager'),
$container->get('module_handler'),
$container->get('config.factory'),
$container->get('asset.location'),
$container->get('current_user'),
$container->has('group.membership') ? $container->get('group.membership') : NULL,
);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Date of birth.
$form['date'] = [
'#type' => 'datetime',
'#title' => $this->t('Date of birth'),
'#default_value' => new DrupalDateTime('midnight', $this->currentUser->getTimeZone()),
'#required' => TRUE,
];
// Number of children.
$range = range(1, 15);
$form['child_count'] = [
'#type' => 'select',
'#title' => $this->t('How many children were born?'),
'#options' => array_combine($range, $range),
'#default_value' => 1,
'#ajax' => [
'callback' => [$this, 'childrenCallback'],
'wrapper' => 'children',
],
];
// Create a container for children.
$form['children'] = [
'#type' => 'container',
'#tree' => TRUE,
'#attributes' => ['id' => 'children'],
];
// Create a fieldset for each child.
$child_count = $form_state->getValue('child_count', 1);
for ($i = 0; $i < $child_count; $i++) {
$counter = ' ' . ($i + 1);
$form['children'][$i] = [
'#type' => 'details',
'#title' => $this->t('Child') . $counter,
'#open' => $i == 0,
];
// Child name.
$form['children'][$i]['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Name'),
'#description' => $this->t('Give the animal a name (and/or tag ID below). If the name is left blank, then it will be copied from the tag ID.'),
];
// Child ID tag.
$form['children'][$i]['tag'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['inline-container'],
],
];
$form['children'][$i]['tag']['type'] = [
'#type' => 'select',
'#title' => $this->t('Tag type'),
'#options' => [NULL => ''] + farm_id_tag_type_options('animal'),
];
$form['children'][$i]['tag']['id'] = [
'#type' => 'textfield',
'#title' => $this->t('Tag ID'),
'#size' => 16,
];
$form['children'][$i]['tag']['location'] = [
'#type' => 'textfield',
'#title' => $this->t('Tag location'),
'#size' => 16,
];
// Male or female.
$form['children'][$i]['sex'] = [
'#type' => 'radios',
'#title' => $this->t('Sex'),
'#options' => [
'F' => $this->t('Female'),
'M' => $this->t('Male'),
],
];
// Birth weight (metric: kg / us: lbs)
$units = $this->birthWeightUnits();
$form['children'][$i]['weight'] = [
'#type' => 'number',
'#title' => $this->t('Birth weight (@units)', ['@units' => $units]),
'#description' => $this->t('This will create a birth weight observation log associated with the child.'),
'#min' => 0,
'#step' => 0.01,
];
// Notes.
$form['children'][$i]['notes'] = [
'#type' => 'text_format',
'#title' => $this->t('Notes about this child'),
'#format' => 'default',
];
// Survived.
$form['children'][$i]['survived'] = [
'#type' => 'checkbox',
'#title' => $this->t('Survived birth'),
'#description' => $this->t('Uncheck this if the child did not survive. The child animal record will still be created, but will be immediately archived.'),
'#default_value' => TRUE,
];
}
// Create vertical tabs.
$form['tabs'] = [
'#type' => 'vertical_tabs',
];
// Create lineage tab.
$form['lineage'] = [
'#type' => 'details',
'#title' => $this->t('Lineage'),
'#group' => 'tabs',
];
// Birth mother.
$form['lineage']['birth_mother'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Birth mother'),
'#description' => $this->t('This is the mother giving birth. She will be referenced on the Birth log that is created.'),
'#target_type' => 'asset',
'#selection_settings' => [
'target_bundles' => ['animal'],
'sort' => [
'field' => 'status',
'direction' => 'ASC',
],
],
];
// Genetic mother.
$form['lineage']['genetic_mother'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Genetic mother'),
'#description' => $this->t("If the genetic mother is different from the birth mother, she can be referenced here for lineage tracking. Otherwise, it will be assumed that the birth mother is the genetic mother. This will be referenced as the child's parent."),
'#target_type' => 'asset',
'#selection_settings' => [
'target_bundles' => ['animal'],
'sort' => [
'field' => 'status',
'direction' => 'ASC',
],
],
];
// Genetic father.
$form['lineage']['genetic_father'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Genetic father'),
'#description' => $this->t("This will be referenced as the child's parent."),
'#target_type' => 'asset',
'#selection_settings' => [
'target_bundles' => ['animal'],
'sort' => [
'field' => 'status',
'direction' => 'ASC',
],
],
];
// If the group module is enabled, add an entity autocomplete field for
// assigning the children to a group.
if ($this->moduleHandler->moduleExists('farm_group')) {
$form['group'] = [
'#type' => 'details',
'#title' => $this->t('Group'),
'#group' => 'tabs',
];
$form['group']['group'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Assign to group'),
'#description' => $this->t('This will make each child a member of the selected group.'),
'#target_type' => 'asset',
'#selection_settings' => [
'target_bundles' => ['group'],
'sort' => [
'field' => 'status',
'direction' => 'ASC',
],
],
];
}
// Birth notes.
$form['notes'] = [
'#type' => 'details',
'#title' => $this->t('Notes'),
'#group' => 'tabs',
];
$form['notes']['notes'] = [
'#type' => 'text_format',
'#title' => $this->t('Notes about the overall birth process'),
'#format' => 'default',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// Iterate over the children.
foreach ($form_state->getValue('children') as $delta => $child) {
// Each child must have a name or tag ID.
if (empty($child['name']) && empty($child['tag']['id'])) {
$form_state->setError($form['children'][$delta]['name'], $this->t('The child must have a name or tag ID.'));
}
}
// A mother (either birth or genetic) must be selected.
if (empty($form_state->getValue('birth_mother')) && empty($form_state->getValue('genetic_mother'))) {
$form_state->setError($form['lineage']['birth_mother'], $this->t('A mother animal must be selected.'));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Get the birthdate.
/** @var \Drupal\Core\Datetime\DrupalDateTime $birthdate */
$birthdate = $form_state->getValue('date');
// Load the mother and father asset(s).
/** @var \Drupal\asset\Entity\AssetInterface|null $birth_mother */
$birth_mother = NULL;
if ($form_state->getValue('birth_mother')) {
$birth_mother = $this->entityTypeManager->getStorage('asset')->load($form_state->getValue('birth_mother'));
}
/** @var \Drupal\asset\Entity\AssetInterface|null $genetic_mother */
$genetic_mother = NULL;
if ($form_state->getValue('genetic_mother')) {
$genetic_mother = $this->entityTypeManager->getStorage('asset')->load($form_state->getValue('genetic_mother'));
}
/** @var \Drupal\asset\Entity\AssetInterface|null $genetic_father */
$genetic_father = NULL;
if ($form_state->getValue('genetic_father')) {
$genetic_father = $this->entityTypeManager->getStorage('asset')->load($form_state->getValue('genetic_father'));
}
// If there is no birth mother, assume that the genetic mother is the birth
// mother. Likewise, if there is no genetic mother, assume that the birth
// mother is the genetic mother. We validate that one of them must exist
// above.
if (empty($birth_mother)) {
$birth_mother = $genetic_mother;
}
if (empty($genetic_mother)) {
$genetic_mother = $birth_mother;
}
// Assemble the list of genetic parents.
$parents = [$genetic_mother];
if (!empty($genetic_father)) {
$parents[] = $genetic_father;
}
// Iterate over the children and create an asset for each.
$children = [];
foreach ($form_state->getValue('children') as $child) {
// Draft a new animal asset for the child.
$asset_values = [
'type' => 'animal',
'name' => !empty($child['name']) ? $child['name'] : $child['tag']['id'],
'animal_type' => $genetic_mother->get('animal_type')->referencedEntities(),
'parent' => $parents,
'birthdate' => $birthdate->getTimestamp(),
'status' => !empty($child['survived']) ? 'active' : 'archived',
];
// Set the sex, if available.
if (!empty($child['sex'])) {
$asset_values['sex'] = $child['sex'];
}
// Set the ID tag, if available.
if (!empty($child['tag']['type']) || !empty($child['tag']['id']) || !empty($child['tag']['location'])) {
$asset_values['id_tag'] = [
[
'type' => $child['tag']['type'],
'id' => $child['tag']['id'],
'location' => $child['tag']['location'],
],
];
}
// Set the child notes, if available.
if (!empty($child['notes']['value'])) {
$asset_values['notes'] = $child['notes'];
}
// Create the child animal asset and add it to the list.
$asset = $this->createAsset($asset_values);
$children[] = $asset;
// If a birth weight was specified, create a weight observation log.
if (!empty($child['weight'])) {
$this->createLog([
'type' => 'observation',
'timestamp' => $birthdate->getTimestamp(),
'name' => $this->t('Weight of @asset is @weight @units', ['@asset' => Markup::create($asset->label()), '@weight' => $child['weight'], '@units' => $this->birthWeightUnits()]),
'asset' => [$asset],
'quantity' => [
[
'type' => 'standard',
'measure' => 'weight',
'value' => $child['weight'],
'units' => $this->birthWeightUnits(),
],
],
'status' => 'done',
]);
}
}
// Draft birth log values.
$birth_log_values = [
'type' => 'birth',
'timestamp' => $birthdate->getTimestamp(),
'asset' => $children,
'mother' => [$birth_mother],
'notes' => $form_state->getValue('notes'),
'status' => 'done',
];
// Generate the birth log name.
$birth_log_values['name'] = $this->t('Birth: @children', ['@children' => Markup::create($this->entityLabelsSummary($children))]);
// If the birth mother has a location (at the time of birth), use the birth
// log to set the location of the children.
$location = $this->assetLocation->getLocation($birth_mother, $birthdate->getTimestamp());
if ($location) {
$birth_log_values['location'] = $location;
$birth_log_values['is_movement'] = TRUE;
}
// If the group module is enabled, check to see if a group was selected, or
// if the birth mother is in a group (at the time of the birth), make the
// log into a group assignment log that references the group.
if ($this->moduleHandler->moduleExists('farm_group')) {
$group = $form_state->getValue('group');
if (!empty($group)) {
$group = [$this->entityTypeManager->getStorage('asset')->load($group)];
}
if (empty($group) && $this->groupMembership !== NULL) {
$group = $this->groupMembership->getGroup($birth_mother, $birthdate->getTimestamp());
}
if (!empty($group)) {
$birth_log_values['group'] = $group;
$birth_log_values['is_group_assignment'] = TRUE;
}
}
// Save the birth log.
$this->createLog($birth_log_values);
}
/**
* Ajax callback for children fields.
*/
public function childrenCallback(array $form, FormStateInterface $form_state) {
return $form['children'];
}
/**
* Helper function for getting the birth weight units.
*
* @return string
* The units name, depending on the system of measurement.
*/
protected function birthWeightUnits() {
$quantity_settings = $this->configFactory->get('quantity.settings');
if ($quantity_settings->get('system_of_measurement') == 'us') {
return 'lbs';
}
return 'kg';
}
}

View File

@@ -0,0 +1,265 @@
<?php
namespace Drupal\Tests\farm_quick_birth\Kernel;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Tests\farm_quick\Kernel\QuickFormTestBase;
use Drupal\asset\Entity\Asset;
use Drupal\log\Entity\Log;
use Drupal\taxonomy\Entity\Term;
/**
* Tests for farmOS birth quick form.
*
* @group farm
*/
class QuickBirthTest extends QuickFormTestBase {
/**
* Quick form ID.
*
* @var string
*/
protected $quickFormId = 'birth';
/**
* Asset location service.
*
* @var \Drupal\farm_location\AssetLocationInterface
*/
protected $assetLocation;
/**
* Group membership service.
*
* @var \Drupal\farm_group\GroupMembershipInterface
*/
protected $groupMembership;
/**
* {@inheritdoc}
*/
protected static $modules = [
'farm_animal',
'farm_animal_type',
'farm_birth',
'farm_group',
'farm_id_tag',
'farm_land',
'farm_observation',
'farm_parent',
'farm_quantity_standard',
'farm_quick_birth',
'farm_unit',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->assetLocation = \Drupal::service('asset.location');
$this->groupMembership = \Drupal::service('group.membership');
$this->installConfig([
'farm_animal',
'farm_animal_type',
'farm_birth',
'farm_group',
'farm_id_tag',
'farm_land',
'farm_observation',
'farm_quantity_standard',
]);
}
/**
* Test birth quick form submission.
*/
public function testQuickBirth() {
// Get today's date.
$today = new DrupalDateTime('midnight');
// Create two animal breeds.
$breed1 = Term::create([
'name' => 'Breed 1',
'vid' => 'animal_type',
]);
$breed1->save();
$breed2 = Term::create([
'name' => 'Breed 2',
'vid' => 'animal_type',
]);
$breed2->save();
// Create birth mother, genetic mother, and genetic father animal assets.
$birth_mother = Asset::create([
'name' => 'Birth Mother',
'type' => 'animal',
'animal_type' => $breed1,
'sex' => 'F',
'status' => 'active',
]);
$birth_mother->save();
$genetic_mother = Asset::create([
'name' => 'Genetic Mother',
'type' => 'animal',
'animal_type' => $breed2,
'sex' => 'F',
'status' => 'active',
]);
$genetic_mother->save();
$genetic_father = Asset::create([
'name' => 'Genetic Father',
'type' => 'animal',
'animal_type' => $breed1,
'sex' => 'M',
'status' => 'active',
]);
$genetic_father->save();
// Create a location asset and move the birth mother there via a log with
// a timestamp of yesterday.
$location = Asset::create([
'name' => 'Field A',
'type' => 'land',
'land_type' => 'field',
'is_fixed' => TRUE,
'is_location' => TRUE,
'status' => 'active',
]);
$location->save();
$movement = Log::create([
'type' => 'observation',
'timestamp' => $today->getTimestamp() - 86400,
'asset' => [$birth_mother],
'location' => [$location],
'is_movement' => TRUE,
'status' => 'done',
]);
$movement->save();
// Create a group asset.
$group = Asset::create([
'name' => 'Herd 1',
'type' => 'group',
'status' => 'active',
]);
$group->save();
// Submit the birth quick form.
$this->submitQuickForm([
'date' => [
'date' => $today->format('Y-m-d'),
'time' => $today->format('H:i:s'),
],
'child_count' => 2,
'children' => [
[
'name' => "Suzie's child",
'tag' => [
'id' => '123',
'type' => 'ear_tag',
'location' => 'Left ear',
],
'sex' => 'F',
'weight' => '10',
'notes' => [
'value' => 'Child 1 notes',
'format' => 'default',
],
'survived' => TRUE,
],
[
'name' => 'Child 2',
// A checkbox with a #default_value of TRUE must pass NULL in order
// to be treated as FALSE due to the core checkbox element value
// callback logic. Setting this to FALSE or 0 does not work.
// @see \Drupal\Core\Render\Element\Checkbox::valueCallback()
'survived' => NULL,
],
],
'birth_mother' => $birth_mother->label(),
'genetic_mother' => $genetic_mother->label(),
'genetic_father' => $genetic_father->label(),
'group' => $group->label(),
'notes' => [
'value' => 'Birth notes',
'format' => 'default',
],
]);
// Load assets and logs.
$assets = $this->assetStorage->loadMultiple();
$logs = $this->logStorage->loadMultiple();
// Confirm that seven assets (5 animals + 1 land + 1 group) and three logs
// (1 birth + 2 observations) exists.
$this->assertCount(7, $assets);
$this->assertCount(3, $logs);
// Confirm that the first child animal asset contains all the expected data.
$child1 = $assets[6];
$this->assertEquals("Suzie's child", $child1->label());
$this->assertEquals($breed2->id(), $child1->get('animal_type')->target_id);
$this->assertEquals($today->getTimestamp(), $child1->get('birthdate')->value);
$this->assertEquals('F', $child1->get('sex')->value);
$this->assertEquals('ear_tag', $child1->get('id_tag')[0]->type);
$this->assertEquals('123', $child1->get('id_tag')[0]->id);
$this->assertEquals('Left ear', $child1->get('id_tag')[0]->location);
$parents = $child1->get('parent')->referencedEntities();
$this->assertCount(2, $parents);
$this->assertEquals($genetic_mother->id(), $parents[0]->id());
$this->assertEquals($genetic_father->id(), $parents[1]->id());
$this->assertEquals('Child 1 notes', $child1->get('notes')->value);
$this->assertEquals('active', $child1->get('status')->value);
$child_location = $this->assetLocation->getLocation($child1);
$this->assertEquals($location->id(), reset($child_location)->id());
$child_group = $this->groupMembership->getGroup($child1);
$this->assertEquals($group->id(), reset($child_group)->id());
// Confirm that the second child animal asset contains all the expected
// data.
$child2 = $assets[7];
$this->assertEquals('Child 2', $child2->label());
$this->assertEquals($breed2->id(), $child2->get('animal_type')->target_id);
$this->assertEquals($today->getTimestamp(), $child2->get('birthdate')->value);
$this->assertEquals('', $child2->get('sex')->value);
$parents = $child2->get('parent')->referencedEntities();
$this->assertCount(2, $parents);
$this->assertEquals($genetic_mother->id(), $parents[0]->id());
$this->assertEquals($genetic_father->id(), $parents[1]->id());
$this->assertEquals('archived', $child2->get('status')->value);
$child_location = $this->assetLocation->getLocation($child2);
$this->assertEquals($location->id(), reset($child_location)->id());
$child_group = $this->groupMembership->getGroup($child2);
$this->assertEquals($group->id(), reset($child_group)->id());
// Confirm that the weight observation log contains all the expected data.
$weight_log = $logs[2];
$this->assertEquals('observation', $weight_log->bundle());
$this->assertEquals($today->getTimestamp(), $weight_log->get('timestamp')->value);
$this->assertEquals("Weight of Suzie's child is 10 kg", $weight_log->label());
$this->assertEquals($child1->id(), $weight_log->get('asset')->referencedEntities()[0]->id());
$this->assertEquals('weight', $weight_log->get('quantity')->referencedEntities()[0]->get('measure')->value);
$this->assertEquals('10', $weight_log->get('quantity')->referencedEntities()[0]->get('value')[0]->get('decimal')->getValue());
$this->assertEquals('kg', $weight_log->get('quantity')->referencedEntities()[0]->get('units')->referencedEntities()[0]->get('name')->value);
$this->assertEquals('done', $weight_log->get('status')->value);
// Confirm that the birth log contains all the expected data.
$birth_log = $logs[3];
$this->assertEquals('birth', $birth_log->bundle());
$this->assertEquals($today->getTimestamp(), $birth_log->get('timestamp')->value);
$this->assertEquals("Birth: Suzie's child, Child 2", $birth_log->label());
$this->assertEquals($child1->id(), $birth_log->get('asset')->referencedEntities()[0]->id());
$this->assertEquals($child2->id(), $birth_log->get('asset')->referencedEntities()[1]->id());
$this->assertEquals($birth_mother->id(), $birth_log->get('mother')->referencedEntities()[0]->id());
$this->assertEquals($location->id(), $birth_log->get('location')[0]->target_id);
$this->assertEquals(TRUE, $birth_log->get('is_movement')->value);
$this->assertEquals($group->id(), $birth_log->get('group')[0]->target_id);
$this->assertEquals(TRUE, $birth_log->get('is_group_assignment')->value);
$this->assertEquals('done', $birth_log->get('status')->value);
$this->assertEquals('Birth notes', $birth_log->get('notes')->value);
}
}

View File

@@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
module:
- asset
- farm_quick_group
id: quick_group
label: 'Assign group membership'
type: asset
plugin: quick_group
configuration: { }

View File

@@ -0,0 +1,4 @@
# Schema for actions.
action.configuration.quick_group:
type: action_configuration_default
label: 'Configuration for the quick group action'

View File

@@ -0,0 +1,9 @@
name: Group Quick Form
description: Provides a quick form for recording asset group membership changes.
type: module
package: farmOS Quick Forms
core_version_requirement: ^10
dependencies:
- farm:farm_group
- farm:farm_observation
- farm:farm_quick

View File

@@ -0,0 +1,27 @@
<?php
/**
* @file
* Post update hooks for the farm_quick_group module.
*/
use Drupal\system\Entity\Action;
/**
* Install system.action.quick_group.
*/
function farm_quick_group_post_update_install_quick_group_action(&$sandbox) {
$config = Action::create([
'id' => 'quick_group',
'label' => 'Assign group membership',
'type' => 'asset',
'plugin' => 'quick_group',
'dependencies' => [
'module' => [
'asset',
'farm_quick_group',
],
],
]);
$config->save();
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\farm_quick_group\Plugin\Action;
use Drupal\farm_quick\Plugin\Action\QuickFormActionBase;
/**
* Action for recording group membership assignment.
*
* @Action(
* id = "quick_group",
* label = @Translation("Assign group membership"),
* type = "asset",
* confirm_form_route_name = "farm.quick.group"
* )
*/
class Group extends QuickFormActionBase {
/**
* {@inheritdoc}
*/
public function getQuickFormId(): string {
return 'group';
}
}

View File

@@ -0,0 +1,235 @@
<?php
namespace Drupal\farm_quick_group\Plugin\QuickForm;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\Session\AccountInterface;
use Drupal\farm_group\GroupMembershipInterface;
use Drupal\farm_quick\Plugin\QuickForm\QuickFormBase;
use Drupal\farm_quick\Plugin\QuickForm\QuickFormInterface;
use Drupal\farm_quick\Traits\QuickFormElementsTrait;
use Drupal\farm_quick\Traits\QuickLogTrait;
use Drupal\farm_quick\Traits\QuickPrepopulateTrait;
use Drupal\farm_quick\Traits\QuickStringTrait;
use Psr\Container\ContainerInterface;
/**
* Group quick form.
*
* @QuickForm(
* id = "group",
* label = @Translation("Group membership"),
* description = @Translation("Record asset group membership changes."),
* helpText = @Translation("Use this form to assign assets to a group. A new observation log will be created to record the group membership change."),
* permissions = {
* "create observation log",
* }
* )
*/
class Group extends QuickFormBase implements QuickFormInterface {
use QuickLogTrait;
use QuickFormElementsTrait;
use QuickPrepopulateTrait;
use QuickStringTrait;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Group membership service.
*
* @var \Drupal\farm_group\GroupMembershipInterface
*/
protected $groupMembership;
/**
* Current user object.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a QuickFormBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\farm_group\GroupMembershipInterface $group_membership
* Group membership service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* Current user object.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MessengerInterface $messenger, EntityTypeManagerInterface $entity_type_manager, GroupMembershipInterface $group_membership, AccountInterface $current_user) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $messenger);
$this->messenger = $messenger;
$this->entityTypeManager = $entity_type_manager;
$this->groupMembership = $group_membership;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('messenger'),
$container->get('entity_type.manager'),
$container->get('group.membership'),
$container->get('current_user'),
);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?string $id = NULL) {
// Date.
$form['date'] = [
'#type' => 'datetime',
'#title' => $this->t('Date'),
'#default_value' => new DrupalDateTime('midnight', $this->currentUser->getTimeZone()),
'#required' => TRUE,
];
// Assets.
$prepopulated_assets = $this->getPrepopulatedEntities('asset', $form_state);
$form['asset'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Assets'),
'#description' => $this->t('Which assets are changing group membership?'),
'#target_type' => 'asset',
'#selection_settings' => [
'sort' => [
'field' => 'status',
'direction' => 'ASC',
],
],
'#maxlength' => 1024,
'#tags' => TRUE,
'#required' => TRUE,
'#default_value' => $prepopulated_assets,
];
// Groups.
$form['group'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Groups'),
'#description' => $this->t('The groups to assign the assets to. Leave blank to un-assign assets from all groups.'),
'#target_type' => 'asset',
'#selection_handler' => 'views',
'#selection_settings' => [
'view' => [
'view_name' => 'farm_group_reference',
'display_name' => 'entity_reference',
'arguments' => [],
],
'match_operator' => 'CONTAINS',
],
'#maxlength' => 1024,
'#tags' => TRUE,
];
// Notes.
$form['notes'] = [
'#type' => 'details',
'#title' => $this->t('Notes'),
];
$form['notes']['notes'] = [
'#type' => 'text_format',
'#title' => $this->t('Notes'),
'#title_display' => 'invisible',
'#format' => 'default',
];
// Done.
$form['done'] = [
'#type' => 'checkbox',
'#title' => $this->t('Completed'),
'#default_value' => TRUE,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Draft a group membership observation log from the user-submitted data.
$timestamp = $form_state->getValue('date')->getTimestamp();
$status = $form_state->getValue('done') ? 'done' : 'pending';
$log = [
'type' => 'observation',
'timestamp' => $timestamp,
'asset' => $form_state->getValue('asset'),
'group' => $form_state->getValue('group'),
'notes' => $form_state->getValue('notes'),
'status' => $status,
'is_group_assignment' => TRUE,
];
// Load assets and groups.
$assets = $this->loadEntityAutocompleteAssets($form_state->getValue('asset'));
$groups = $this->loadEntityAutocompleteAssets($form_state->getValue('group'));
// Generate a name for the log.
$asset_names = $this->entityLabelsSummary($assets);
$group_names = $this->entityLabelsSummary($groups);
$log['name'] = $this->t('Clear group membership of @assets', ['@assets' => Markup::create($asset_names)]);
if (!empty($group_names)) {
$log['name'] = $this->t('Group @assets into @groups', ['@assets' => Markup::create($asset_names), '@groups' => Markup::create($group_names)]);
}
// Create the log.
$this->createLog($log);
}
/**
* Load assets from entity_autocomplete values.
*
* @param array|null $values
* The value from $form_state->getValue().
*
* @return \Drupal\asset\Entity\AssetInterface[]
* Returns an array of assets.
*/
protected function loadEntityAutocompleteAssets($values) {
$entities = [];
if (empty($values)) {
return $entities;
}
foreach ($values as $value) {
if ($value instanceof EntityInterface) {
$entities[] = $value;
}
elseif (!empty($value['target_id'])) {
$entities[] = $this->entityTypeManager->getStorage('asset')->load($value['target_id']);
}
}
return $entities;
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace Drupal\Tests\farm_quick_group\Kernel;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Tests\farm_quick\Kernel\QuickFormTestBase;
use Drupal\asset\Entity\Asset;
/**
* Tests for farmOS group quick form.
*
* @group farm
*/
class QuickGroupTest extends QuickFormTestBase {
/**
* Quick form ID.
*
* @var string
*/
protected $quickFormId = 'group';
/**
* {@inheritdoc}
*/
protected static $modules = [
'farm_equipment',
'farm_group',
'farm_observation',
'farm_quick_group',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig([
'farm_equipment',
'farm_group',
'farm_observation',
]);
}
/**
* Test group quick form submission.
*/
public function testQuickGroup() {
// Get today's date.
$today = new DrupalDateTime('midnight');
// Create two equipment assets and two group assets.
$equipment1 = Asset::create([
'name' => 'Tractor',
'type' => 'equipment',
'status' => 'active',
]);
$equipment1->save();
$equipment2 = Asset::create([
'name' => "Mike's Combine",
'type' => 'equipment',
'status' => 'active',
]);
$equipment2->save();
$group1 = Asset::create([
'name' => 'Group 1',
'type' => 'group',
'status' => 'active',
]);
$group1->save();
$group2 = Asset::create([
'name' => 'Group 2',
'type' => 'group',
'status' => 'active',
]);
$group2->save();
// Programmatically submit the group quick form.
$form_values = [
'date' => [
'date' => $today->format('Y-m-d'),
'time' => $today->format('H:i:s'),
],
'asset' => [
['target_id' => $equipment1->id()],
['target_id' => $equipment2->id()],
],
'group' => [
['target_id' => $group1->id()],
['target_id' => $group2->id()],
],
'notes' => [
'value' => 'Lorem ipsum',
'format' => 'default',
],
'done' => TRUE,
];
$this->submitQuickForm($form_values);
// Load logs.
$logs = $this->logStorage->loadMultiple();
// Confirm that one log exists.
$this->assertCount(1, $logs);
// Check that the observation log's fields were populated correctly.
$log = $logs[1];
$this->assertEquals('observation', $log->bundle());
$this->assertEquals($today->getTimestamp(), $log->get('timestamp')->value);
$this->assertEquals("Group Tractor, Mike's Combine into Group 1, Group 2", $log->label());
$this->assertEquals($equipment1->id(), $log->get('asset')->referencedEntities()[0]->id());
$this->assertEquals($equipment2->id(), $log->get('asset')->referencedEntities()[1]->id());
$this->assertEquals($group1->id(), $log->get('group')->referencedEntities()[0]->id());
$this->assertEquals($group2->id(), $log->get('group')->referencedEntities()[1]->id());
$this->assertEquals('Lorem ipsum', $log->get('notes')->value);
$this->assertEquals('done', $log->get('status')->value);
}
}

View File

@@ -0,0 +1,19 @@
farm_quick.settings.inventory:
type: quick_form_settings
label: 'Inventory quick form settings'
mapping:
asset:
type: integer
label: 'Default asset ID'
units:
type: string
label: 'Default quantity units'
measure:
type: string
label: 'Default quantity measure'
inventory_adjustment:
type: string
label: 'Default inventory adjustment type'
log_type:
type: string
label: 'Default log type'

View File

@@ -0,0 +1,9 @@
name: Inventory Quick Form
description: Provides a quick form for recording asset inventory adjustments.
type: module
package: farmOS Quick Forms
core_version_requirement: ^10
dependencies:
- farm:farm_inventory
- farm:farm_observation
- farm:farm_quick

View File

@@ -0,0 +1,520 @@
<?php
namespace Drupal\farm_quick_inventory\Plugin\QuickForm;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\Session\AccountInterface;
use Drupal\asset\Entity\AssetInterface;
use Drupal\farm_inventory\AssetInventoryInterface;
use Drupal\farm_quick\Plugin\QuickForm\ConfigurableQuickFormInterface;
use Drupal\farm_quick\Plugin\QuickForm\QuickFormBase;
use Drupal\farm_quick\Traits\ConfigurableQuickFormTrait;
use Drupal\farm_quick\Traits\QuickFormElementsTrait;
use Drupal\farm_quick\Traits\QuickLogTrait;
use Drupal\farm_quick\Traits\QuickTermTrait;
use Drupal\log\Entity\Log;
use Drupal\taxonomy\TermInterface;
use Psr\Container\ContainerInterface;
/**
* Inventory quick form.
*
* @QuickForm(
* id = "inventory",
* label = @Translation("Inventory"),
* description = @Translation("Record asset inventory adjustments."),
* helpText = @Translation("Use this form to increment, decrement, or reset the inventory of an asset. A new log will be created to record the adjustment."),
* permissions = {}
* )
*/
class Inventory extends QuickFormBase implements ConfigurableQuickFormInterface {
use ConfigurableQuickFormTrait;
use QuickLogTrait;
use QuickFormElementsTrait;
use QuickTermTrait;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Asset inventory service.
*
* @var \Drupal\farm_inventory\AssetInventoryInterface
*/
protected $assetInventory;
/**
* Current user object.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a QuickFormBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\farm_inventory\AssetInventoryInterface $asset_inventory
* Asset inventory service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* Current user object.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MessengerInterface $messenger, EntityTypeManagerInterface $entity_type_manager, AssetInventoryInterface $asset_inventory, AccountInterface $current_user) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $messenger);
$this->messenger = $messenger;
$this->entityTypeManager = $entity_type_manager;
$this->assetInventory = $asset_inventory;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('messenger'),
$container->get('entity_type.manager'),
$container->get('asset.inventory'),
$container->get('current_user'),
);
}
/**
* {@inheritdoc}
*/
public function access(AccountInterface $account) {
// Check to ensure the user has permission to create the configured log type
// and view the configured asset.
$result = AccessResult::allowedIf($this->entityTypeManager->getAccessControlHandler('log')->createAccess($this->configuration['log_type'], $account));
if (!empty($this->configuration['asset'])) {
$asset = $this->entityTypeManager->getStorage('asset')->load($this->configuration['asset']);
$result = $result->andIf(AccessResult::allowedIf($asset->access('view')));
}
return $result;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'asset' => NULL,
'measure' => NULL,
'units' => NULL,
'inventory_adjustment' => 'reset',
'log_type' => 'observation',
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?string $id = NULL) {
// Date.
$form['date'] = [
'#type' => 'datetime',
'#title' => $this->t('Date'),
'#default_value' => new DrupalDateTime('midnight', $this->currentUser->getTimeZone()),
'#required' => TRUE,
];
// Asset.
$form['asset'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Asset'),
'#description' => $this->t("Which asset's inventory is being adjusted?"),
'#target_type' => 'asset',
'#selection_settings' => [
'sort' => [
'field' => 'status',
'direction' => 'ASC',
],
],
'#maxlength' => 1024,
'#required' => TRUE,
];
if (!empty($this->configuration['asset'])) {
$form['asset']['#default_value'] = $this->entityTypeManager->getStorage('asset')->load($this->configuration['asset']);
}
// Quantity.
$form['quantity'] = $this->buildInlineContainer();
$form['quantity']['#tree'] = TRUE;
$form['quantity']['value'] = [
'#type' => 'textfield',
'#title' => $this->t('Quantity'),
'#size' => 16,
'#required' => TRUE,
];
$form['quantity']['units'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Units'),
'#target_type' => 'taxonomy_term',
'#selection_settings' => [
'target_bundles' => ['unit'],
],
'#autocreate' => [
'bundle' => 'unit',
],
'#size' => 16,
];
if (!empty($this->configuration['units'])) {
$form['quantity']['units']['#default_value'] = $this->createOrLoadTerm($this->configuration['units'], 'unit');
}
$form['quantity']['measure'] = [
'#type' => 'select',
'#title' => $this->t('Measure'),
'#options' => array_merge(['' => ''], quantity_measure_options()),
'#default_value' => $this->configuration['measure'],
];
// Inventory adjustment.
$form['inventory_adjustment'] = [
'#type' => 'select',
'#title' => $this->t('Adjustment type'),
'#description' => $this->t('What type of inventory adjustment is this?'),
'#options' => [
'increment' => $this->t('Increment'),
'decrement' => $this->t('Decrement'),
'reset' => $this->t('Reset'),
],
'#required' => TRUE,
'#default_value' => $this->configuration['inventory_adjustment'],
];
// Notes.
$form['notes'] = [
'#type' => 'details',
'#title' => $this->t('Notes'),
];
$form['notes']['notes'] = [
'#type' => 'text_format',
'#title' => $this->t('Notes'),
'#title_display' => 'invisible',
'#format' => 'default',
];
// Advanced.
$form['advanced'] = [
'#type' => 'details',
'#title' => $this->t('Advanced'),
];
// Log type.
$form['advanced']['log_type'] = [
'#type' => 'select',
'#title' => $this->t('Log type'),
'#description' => $this->t('Select the type of log to create.'),
'#options' => $this->logTypeOptions(),
'#required' => TRUE,
'#default_value' => $this->configuration['log_type'],
];
// Log name.
// Provide a checkbox to allow customizing this. Otherwise, it will be
// automatically generated on submission.
$form['advanced']['custom_name'] = [
'#type' => 'checkbox',
'#title' => $this->t('Customize log name'),
'#description' => $this->t('This allows the log name to be customized. Otherwise, a default name will be generated.'),
'#default_value' => FALSE,
'#ajax' => [
'callback' => [$this, 'logNameCallback'],
'wrapper' => 'log-name',
],
];
$form['advanced']['name_wrapper'] = [
'#type' => 'container',
'#attributes' => ['id' => 'log-name'],
];
if ($form_state->getValue('custom_name', FALSE)) {
$form['advanced']['name_wrapper']['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Log name'),
'#maxlength' => 255,
'#default_value' => $this->generateLogName($form_state),
'#required' => TRUE,
];
}
// Done.
$form['done'] = [
'#type' => 'checkbox',
'#title' => $this->t('Completed'),
'#default_value' => TRUE,
];
return $form;
}
/**
* Build a list of log type options.
*
* @return array
* Returns an array of log type labels, keyed by machine name.
* Only log types that the user has access to create will be included.
*/
protected function logTypeOptions() {
$log_access_control_handler = $this->entityTypeManager->getAccessControlHandler('log');
$log_types = array_filter($this->entityTypeManager->getStorage('log_type')->loadMultiple(), function ($log_type) use ($log_access_control_handler) {
return $log_access_control_handler->createAccess($log_type->id(), $this->currentUser);
});
return array_map(function ($log_type) {
return $log_type->label();
}, $log_types);
}
/**
* Generate log name.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
*
* @return string
* Returns a log name string.
*/
protected function generateLogName(FormStateInterface $form_state) {
$log_name = '';
// Get the asset name. If an asset has not been selected, bail.
$asset = $form_state->getValue('asset');
if (is_numeric($asset)) {
$asset = $this->entityTypeManager->getStorage('asset')->load($asset);
}
if (!($asset instanceof AssetInterface)) {
return $log_name;
}
// Create a summary of the quantity.
$quantity_summary = $form_state->getValue(['quantity', 'value']);
$units = $form_state->getValue(['quantity', 'units']);
$measure = $form_state->getValue(['quantity', 'measure']);
if (!empty($units)) {
if (is_numeric($units)) {
$units = $this->entityTypeManager->getStorage('taxonomy_term')->load($units);
}
elseif (is_array($units) && !empty($units['entity'])) {
$units = $units['entity'];
}
if ($units instanceof TermInterface) {
$quantity_summary .= ' ' . $units->label();
}
}
if (!empty($measure)) {
$quantity_summary .= ' (' . $measure . ')';
}
// Generate the log name based on the inventory adjustment type.
switch ($form_state->getValue('inventory_adjustment')) {
case 'increment':
$log_name = $this->t('Increment inventory of @asset by @quantity', ['@asset' => Markup::create($asset->label()), '@quantity' => $quantity_summary]);
break;
case 'decrement':
$log_name = $this->t('Decrement inventory of @asset by @quantity', ['@asset' => Markup::create($asset->label()), '@quantity' => $quantity_summary]);
break;
case 'reset':
$log_name = $this->t('Reset inventory of @asset to @quantity', ['@asset' => Markup::create($asset->label()), '@quantity' => $quantity_summary]);
break;
}
return $log_name;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// Mock a minimal log of the selected type to ensure that it validates. This
// protects against creating log types that have required fields that this
// form is not able to populate.
$log = Log::create([
'type' => $form_state->getValue('log_type'),
]);
$violations = $log->validate();
if ($violations->count()) {
$form_state->setError($form['log_type'], $this->t('The selected log type cannot be created. It may have required fields that this form is unable to populate.'));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Load asset.
$asset = $this->entityTypeManager->getStorage('asset')->load($form_state->getValue('asset'));
// Load units term (if specified).
$units = $form_state->getValue(['quantity', 'units']);
if (is_numeric($units)) {
$units = $this->entityTypeManager->getStorage('taxonomy_term')->load($form_state->getValue(['quantity', 'units']));
}
elseif (is_array($units) && !empty($units['entity'])) {
$units = $units['entity'];
}
// Create a quantity for the inventory adjustment.
$quantity = [
'measure' => $form_state->getValue(['quantity', 'measure']),
'value' => $form_state->getValue(['quantity', 'value']),
'units' => $units,
'inventory_adjustment' => $form_state->getValue('inventory_adjustment'),
'inventory_asset' => $asset,
];
// Draft an inventory adjustment log from the user-submitted data.
$timestamp = $form_state->getValue('date')->getTimestamp();
$status = $form_state->getValue('done') ? 'done' : 'pending';
$log = [
'type' => $form_state->getValue('log_type'),
'timestamp' => $timestamp,
'quantity' => [$quantity],
'notes' => $form_state->getValue('notes'),
'status' => $status,
];
// Generate a name for the log.
// If a custom plant name was provided, use that. Otherwise, generate one.
$log['name'] = $this->generateLogName($form_state);
if (!empty($form_state->getValue('custom_name', FALSE)) && $form_state->hasValue('name')) {
$log['name'] = $form_state->getValue('name');
}
// Create the log.
$this->createLog($log);
}
/**
* Ajax callback for log name field.
*/
public function logNameCallback(array $form, FormStateInterface $form_state) {
return $form['advanced']['name_wrapper'];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
// Asset.
$form['asset'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Asset'),
'#description' => $this->t("Which asset's inventory is being adjusted?"),
'#target_type' => 'asset',
'#selection_settings' => [
'sort' => [
'field' => 'status',
'direction' => 'ASC',
],
],
'#maxlength' => 1024,
];
if (!empty($this->configuration['asset'])) {
$form['asset']['#default_value'] = $this->entityTypeManager->getStorage('asset')->load($this->configuration['asset']);
}
// Units.
$form['units'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Units'),
'#target_type' => 'taxonomy_term',
'#selection_settings' => [
'target_bundles' => ['unit'],
],
'#autocreate' => [
'bundle' => 'unit',
],
'#size' => 16,
];
if (!empty($this->configuration['units'])) {
$form['units']['#default_value'] = $this->createOrLoadTerm($this->configuration['units'], 'unit');
}
// Measure.
$form['measure'] = [
'#type' => 'select',
'#title' => $this->t('Measure'),
'#options' => array_merge(['' => ''], quantity_measure_options()),
'#default_value' => $this->configuration['measure'],
];
// Inventory adjustment.
$form['inventory_adjustment'] = [
'#type' => 'select',
'#title' => $this->t('Adjustment type'),
'#description' => $this->t('What type of inventory adjustment is this?'),
'#options' => [
'increment' => $this->t('Increment'),
'decrement' => $this->t('Decrement'),
'reset' => $this->t('Reset'),
],
'#default_value' => $this->configuration['inventory_adjustment'],
];
// Log type.
$form['log_type'] = [
'#type' => 'select',
'#title' => $this->t('Log type'),
'#description' => $this->t('Select the type of log to create.'),
'#options' => $this->logTypeOptions(),
'#default_value' => $this->configuration['log_type'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['asset'] = $form_state->getValue('asset');
$this->configuration['units'] = NULL;
if (!empty($form_state->getValue('units'))) {
// Existing terms will be represented as a numeric term ID.
if (is_numeric($form_state->getValue('units'))) {
$term = $this->entityTypeManager->getStorage('taxonomy_term')->load($form_state->getValue('units'));
if (!empty($term)) {
$this->configuration['units'] = $term->label();
}
}
// New terms will be represented as a term entity object.
elseif (!empty($form_state->getValue('units')['entity'])) {
$this->configuration['units'] = $form_state->getValue('units')['entity']->label();
}
}
$this->configuration['measure'] = $form_state->getValue('measure');
$this->configuration['inventory_adjustment'] = $form_state->getValue('inventory_adjustment');
$this->configuration['log_type'] = $form_state->getValue('log_type');
}
}

View File

@@ -0,0 +1,270 @@
<?php
namespace Drupal\Tests\farm_quick_inventory\Kernel;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Tests\farm_quick\Kernel\QuickFormTestBase;
use Drupal\asset\Entity\Asset;
use Drupal\taxonomy\Entity\Term;
/**
* Tests for farmOS inventory quick form.
*
* @group farm
*/
class QuickInventoryTest extends QuickFormTestBase {
/**
* Quick form ID.
*
* @var string
*/
protected $quickFormId = 'inventory';
/**
* {@inheritdoc}
*/
protected static $modules = [
'farm_activity',
'farm_equipment',
'farm_inventory',
'farm_observation',
'farm_quantity_standard',
'farm_quick_inventory',
'farm_unit',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig([
'farm_activity',
'farm_equipment',
'farm_observation',
'farm_quantity_standard',
'farm_unit',
]);
}
/**
* Test inventory quick form submission.
*/
public function testQuickInventory() {
// Get today's date.
$today = new DrupalDateTime('midnight');
// Create an equipment asset.
$equipment = Asset::create([
'name' => 'Tractor',
'type' => 'equipment',
'status' => 'active',
]);
$equipment->save();
// Programmatically submit the inventory quick form (reset to 1).
$form_values = [
'date' => [
'date' => $today->format('Y-m-d'),
'time' => $today->format('H:i:s'),
],
'asset' => [
['target_id' => $equipment->id()],
],
'quantity' => [
'value' => '1',
'units' => '',
'measure' => '',
],
'inventory_adjustment' => 'reset',
'notes' => [
'value' => 'Lorem ipsum',
'format' => 'default',
],
'log_type' => 'observation',
'done' => TRUE,
];
$this->submitQuickForm($form_values);
// Load logs.
$logs = $this->logStorage->loadMultiple();
// Confirm that one log exists.
$this->assertCount(1, $logs);
// Check that the log's fields were populated correctly.
$log = $logs[1];
$this->assertEquals('observation', $log->bundle());
$this->assertEquals($today->getTimestamp(), $log->get('timestamp')->value);
$this->assertEquals('Reset inventory of Tractor to 1', $log->label());
$this->assertEquals('1', $log->get('quantity')->referencedEntities()[0]->get('value')[0]->get('decimal')->getValue());
$this->assertCount(0, $log->get('quantity')->referencedEntities()[0]->get('units')->referencedEntities());
$this->assertEquals('', $log->get('quantity')->referencedEntities()[0]->get('measure')->value);
$this->assertEquals('reset', $log->get('quantity')->referencedEntities()[0]->get('inventory_adjustment')->value);
$this->assertEquals($equipment->id(), $log->get('quantity')->referencedEntities()[0]->get('inventory_asset')->referencedEntities()[0]->id());
$this->assertEquals('Lorem ipsum', $log->get('notes')->value);
$this->assertEquals('done', $log->get('status')->value);
// Check that the asset has a single inventory of 1.
$inventory = \Drupal::service('asset.inventory')->getInventory($equipment);
$this->assertCount(1, $inventory);
$this->assertEquals('1', $inventory[0]['value']);
$this->assertEquals('', $inventory[0]['units']);
$this->assertEquals('', $inventory[0]['measure']);
// Programmatically submit the inventory quick form (increment by 1 with an
// activity log).
$form_values = [
'date' => [
'date' => $today->format('Y-m-d'),
'time' => $today->format('H:i:s'),
],
'asset' => [
['target_id' => $equipment->id()],
],
'quantity' => [
'value' => '1',
'units' => '',
'measure' => '',
],
'inventory_adjustment' => 'increment',
'log_type' => 'activity',
'done' => TRUE,
];
$this->submitQuickForm($form_values);
// Confirm that two logs exists.
$logs = $this->logStorage->loadMultiple();
$this->assertCount(2, $logs);
// Check that the log is an activity and that the name was populated
// correctly.
$log = $logs[2];
$this->assertEquals('activity', $log->bundle());
$this->assertEquals('Increment inventory of Tractor by 1', $log->label());
// Check that the asset has a single inventory of 2.
$inventory = \Drupal::service('asset.inventory')->getInventory($equipment);
$this->assertCount(1, $inventory);
$this->assertEquals('2', $inventory[0]['value']);
$this->assertEquals('', $inventory[0]['units']);
$this->assertEquals('', $inventory[0]['measure']);
// Programmatically submit the inventory quick form (decrement by 1).
$form_values = [
'date' => [
'date' => $today->format('Y-m-d'),
'time' => $today->format('H:i:s'),
],
'asset' => [
['target_id' => $equipment->id()],
],
'quantity' => [
'value' => '1',
'units' => '',
'measure' => '',
],
'inventory_adjustment' => 'decrement',
'log_type' => 'observation',
'done' => TRUE,
];
$this->submitQuickForm($form_values);
// Confirm that three logs exists.
$logs = $this->logStorage->loadMultiple();
$this->assertCount(3, $logs);
// Check that the log name was populated correctly.
$log = $logs[3];
$this->assertEquals('Decrement inventory of Tractor by 1', $log->label());
// Check that the asset has a single inventory of 1.
$inventory = \Drupal::service('asset.inventory')->getInventory($equipment);
$this->assertCount(1, $inventory);
$this->assertEquals('1', $inventory[0]['value']);
$this->assertEquals('', $inventory[0]['units']);
$this->assertEquals('', $inventory[0]['measure']);
// Create a unit term.
$unit = Term::create([
'name' => 'liters',
'vid' => 'unit',
]);
$unit->save();
// Programmatically submit the inventory quick form with units and measure.
$form_values = [
'date' => [
'date' => $today->format('Y-m-d'),
'time' => $today->format('H:i:s'),
],
'asset' => [
['target_id' => $equipment->id()],
],
'quantity' => [
'value' => '10',
'units' => [
['target_id' => $unit->id()],
],
'measure' => 'volume',
],
'inventory_adjustment' => 'reset',
'log_type' => 'observation',
'done' => TRUE,
];
$this->submitQuickForm($form_values);
// Confirm that four logs exists.
$logs = $this->logStorage->loadMultiple();
$this->assertCount(4, $logs);
// Check that the log's name and quantity measure and units were populated.
$log = $logs[4];
$this->assertEquals('Reset inventory of Tractor to 10 liters (volume)', $log->label());
$this->assertEquals('liters', $log->get('quantity')->referencedEntities()[0]->get('units')->referencedEntities()[0]->get('name')->value);
$this->assertEquals('volume', $log->get('quantity')->referencedEntities()[0]->get('measure')->value);
// Check that the asset has two inventories.
$inventory = \Drupal::service('asset.inventory')->getInventory($equipment);
$this->assertCount(2, $inventory);
// Load the volume (liters) inventory and confirm that it is 10.
$inventory = \Drupal::service('asset.inventory')->getInventory($equipment, 'volume', $unit->id());
$this->assertEquals('volume', $inventory[0]['measure']);
$this->assertEquals('liters', $inventory[0]['units']);
$this->assertEquals('10', $inventory[0]['value']);
// Test customizing the log name.
$form_values = [
'date' => [
'date' => $today->format('Y-m-d'),
'time' => $today->format('H:i:s'),
],
'asset' => [
['target_id' => $equipment->id()],
],
'quantity' => [
'value' => '1',
'units' => '',
'measure' => '',
],
'inventory_adjustment' => 'reset',
'log_type' => 'observation',
'done' => TRUE,
'custom_name' => TRUE,
'name' => 'Test custom log name',
];
$this->submitQuickForm($form_values);
// Confirm that five logs exists.
$logs = $this->logStorage->loadMultiple();
$this->assertCount(5, $logs);
// Check that the log name was populated correctly.
$log = $logs[5];
$this->assertEquals('Test custom log name', $log->label());
}
}

View File

@@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
enforced:
module:
- farm_quick_movement
id: quick_movement
label: Quick movement
description: 'Refreshes map from movement quick form inputs.'
library: 'farm_quick_movement/behavior_quick_movement'
settings: { }

View File

@@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
module:
- asset
- farm_quick_movement
id: quick_movement
label: 'Record movement'
type: asset
plugin: quick_movement
configuration: { }

View File

@@ -0,0 +1,4 @@
# Schema for actions.
action.configuration.quick_movement:
type: action_configuration_default
label: 'Configuration for the quick movement action'

View File

@@ -0,0 +1,8 @@
name: Movement Quick Form
description: Provides a quick form for recording asset movements.
type: module
package: farmOS Quick Forms
core_version_requirement: ^10
dependencies:
- farm:farm_activity
- farm:farm_quick

View File

@@ -0,0 +1,8 @@
quick_movement:
js:
js/quick_movement.js: { }
behavior_quick_movement:
js:
js/farmOS.map.behaviors.quick_movement.js: { }
dependencies:
- farm_map/farm_map

View File

@@ -0,0 +1,24 @@
<?php
/**
* @file
* Contains farm_quick_movement.module.
*/
use Drupal\Core\Entity\EntityTypeInterface;
/**
* Implements hook_entity_base_field_info_alter().
*/
function farm_quick_movement_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) {
/** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
// Add "Move" button to asset "Current location" field formatter which
// redirects to the Movement quick form.
if ($entity_type->id() == 'asset' && !empty($fields['location'])) {
$display_options = $fields['location']->getDisplayOptions('view');
$display_options['type'] = 'asset_current_location_move';
$display_options['settings']['move_asset_button'] = TRUE;
$fields['location']->setDisplayOptions('view', $display_options);
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* @file
* Post update hooks for the farm_quick_movement module.
*/
use Drupal\system\Entity\Action;
/**
* Install system.action.quick_movement.
*/
function farm_quick_movement_post_update_install_quick_movement_action(&$sandbox) {
$config = Action::create([
'id' => 'quick_movement',
'label' => 'Record movement',
'type' => 'asset',
'plugin' => 'quick_movement',
'dependencies' => [
'module' => [
'asset',
'farm_quick_movement',
],
],
]);
$config->save();
}

View File

@@ -0,0 +1,34 @@
(function () {
farmOS.map.behaviors.quick_movement = {
attach: function (instance) {
// Create a layer for the current asset location.
var opts = {
title: 'Current Location',
color: 'blue',
};
instance.currentLocationLayer = instance.addLayer('vector', opts);
// If an asset geometry was pre-populated, add it to the layer.
if (instance.farmMapSettings.behaviors.quick_movement.asset_geometry) {
this.updateAssetGeometry(instance, instance.farmMapSettings.behaviors.quick_movement.asset_geometry)
}
},
// When updating asset geometry, update the current location layer.
updateAssetGeometry: function (instance, wkt) {
// Clear features from the layer.
instance.currentLocationLayer.getSource().clear();
// If WKT is not empty, add features to the layer and zoom.
if (wkt) {
instance.currentLocationLayer.getSource().addFeatures(instance.readFeatures('wkt', wkt));
instance.zoomToLayer(instance.currentLocationLayer);
}
},
// Make sure this runs after farmOS.map.behaviors.wkt.
weight: 101,
};
}());

View File

@@ -0,0 +1,32 @@
(function (Drupal) {
Drupal.behaviors.quick_movement = {
attach: function (context, settings) {
// Only run this when the asset geometry or location geometry wrappers
// are loaded/reloaded.
if (!context.dataset || !(context.dataset.movementGeometry === 'asset-geometry' || context.dataset.movementGeometry === 'location-geometry')) {
return;
}
// Get WKT from the hidden input field.
var wkt = context.querySelector('input').value;
// Get the farmOS-map element and instance.
var element = context.parentElement.querySelector('[data-drupal-selector="edit-geometry-map"]');
var instance = farmOS.map.instances[farmOS.map.targetIndex(element)];
// If this is asset geometry, refresh the map asset geometry.
if (context.dataset.movementGeometry === 'asset-geometry') {
farmOS.map.behaviors.quick_movement.updateAssetGeometry(instance, wkt);
}
// If this is location geometry, copy WKT into the map's value field and
// dispatch the input event so that the input behavior refreshes the map.
if (context.dataset.movementGeometry === 'location-geometry') {
var input = context.parentElement.querySelector('[data-drupal-selector="edit-geometry-value"]');
input.value = wkt;
input.dispatchEvent(new Event('input'));
}
}
};
}(Drupal));

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\farm_quick_movement\Plugin\Action;
use Drupal\farm_quick\Plugin\Action\QuickFormActionBase;
/**
* Action for recording movements.
*
* @Action(
* id = "quick_movement",
* label = @Translation("Record movement"),
* type = "asset",
* confirm_form_route_name = "farm.quick.movement"
* )
*/
class Movement extends QuickFormActionBase {
/**
* {@inheritdoc}
*/
public function getQuickFormId(): string {
return 'movement';
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Drupal\farm_quick_movement\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\farm_location\Plugin\Field\FieldFormatter\AssetCurrentLocationFormatter;
/**
* Field formatter for the current location asset field with a move button.
*
* @FieldFormatter(
* id = "asset_current_location_move",
* label = @Translation("Asset current location (with Move button)"),
* description = @Translation("Display the label of the referenced entities with a button to move."),
* field_types = {
* "entity_reference"
* }
* )
*/
class AssetCurrentLocationMoveFormatter extends AssetCurrentLocationFormatter {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'move_asset_button' => FALSE,
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$elements = parent::settingsForm($form, $form_state);
$elements['move_asset_button'] = [
'#title' => $this->t('Move asset button'),
'#description' => $this->t('Include a button to move the asset.'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('move_asset_button'),
];
return $elements;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
$summary[] = $this->getSetting('move_asset_button') ? $this->t('Include move asset button') : $this->t('No move asset button');
return $summary;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
// Build labels in parent.
$elements = parent::viewElements($items, $langcode);
// Get the asset.
$asset = $items->getEntity();
// If the asset is fixed don't render additional information.
if ($asset->get('is_fixed')->value) {
return $elements;
}
// If there are no current locations only render if configured to.
if (empty($elements) && !$this->getSetting('render_without_location')) {
return $elements;
}
// Add the move asset button if configured.
if ($this->getSetting('move_asset_button')) {
// Append a "Move asset" link.
$options = [
'query' => [
'asset' => $asset->id(),
'destination' => $asset->toUrl()->toString(),
],
];
$elements[] = [
'#type' => 'link',
'#title' => $this->t('Move asset'),
'#url' => Url::fromRoute('farm.quick.movement', [], $options),
'#attributes' => [
'class' => ['button', 'button--small'],
],
];
}
return $elements;
}
}

View File

@@ -0,0 +1,338 @@
<?php
namespace Drupal\farm_quick_movement\Plugin\QuickForm;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\Session\AccountInterface;
use Drupal\farm_geo\Traits\WktTrait;
use Drupal\farm_location\AssetLocationInterface;
use Drupal\farm_quick\Plugin\QuickForm\QuickFormBase;
use Drupal\farm_quick\Plugin\QuickForm\QuickFormInterface;
use Drupal\farm_quick\Traits\QuickFormElementsTrait;
use Drupal\farm_quick\Traits\QuickLogTrait;
use Drupal\farm_quick\Traits\QuickPrepopulateTrait;
use Drupal\farm_quick\Traits\QuickStringTrait;
use Psr\Container\ContainerInterface;
/**
* Movement quick form.
*
* @QuickForm(
* id = "movement",
* label = @Translation("Movement"),
* description = @Translation("Record the movement of assets."),
* helpText = @Translation("Use this form to record the movement of assets to a new location."),
* permissions = {
* "create activity log",
* }
* )
*/
class Movement extends QuickFormBase implements QuickFormInterface {
use QuickLogTrait;
use QuickFormElementsTrait;
use QuickPrepopulateTrait;
use QuickStringTrait;
use WktTrait;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Asset location service.
*
* @var \Drupal\farm_location\AssetLocationInterface
*/
protected $assetLocation;
/**
* Current user object.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a QuickFormBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\farm_location\AssetLocationInterface $asset_location
* Asset location service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* Current user object.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MessengerInterface $messenger, EntityTypeManagerInterface $entity_type_manager, AssetLocationInterface $asset_location, AccountInterface $current_user) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $messenger);
$this->messenger = $messenger;
$this->entityTypeManager = $entity_type_manager;
$this->assetLocation = $asset_location;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('messenger'),
$container->get('entity_type.manager'),
$container->get('asset.location'),
$container->get('current_user'),
);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?string $id = NULL) {
// Date.
$form['date'] = [
'#type' => 'datetime',
'#title' => $this->t('Date'),
'#default_value' => new DrupalDateTime('midnight', $this->currentUser->getTimeZone()),
'#required' => TRUE,
];
// Assets.
$prepopulated_assets = $this->getPrepopulatedEntities('asset', $form_state);
$form['asset'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Assets'),
'#description' => $this->t('Which assets are moving?'),
'#target_type' => 'asset',
'#selection_settings' => [
'sort' => [
'field' => 'status',
'direction' => 'ASC',
],
],
'#maxlength' => 1024,
'#tags' => TRUE,
'#required' => TRUE,
'#ajax' => [
'callback' => [$this, 'assetGeometryCallback'],
'wrapper' => 'asset-geometry',
'event' => 'autocompleteclose change',
],
'#default_value' => $prepopulated_assets,
];
// Locations.
$form['location'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Locations'),
'#description' => $this->t('Where are the assets moving to?'),
'#target_type' => 'asset',
'#selection_handler' => 'views',
'#selection_settings' => [
'view' => [
'view_name' => 'farm_location_reference',
'display_name' => 'entity_reference',
'arguments' => [],
],
'match_operator' => 'CONTAINS',
],
'#maxlength' => 1024,
'#tags' => TRUE,
'#ajax' => [
'callback' => [$this, 'locationGeometryCallback'],
'wrapper' => 'location-geometry',
'event' => 'autocompleteclose change',
],
];
// Geometry.
$form['geometry'] = [
'#type' => 'farm_map_input',
'#title' => $this->t('Geometry'),
'#description' => $this->t('The current geometry of the assets is blue. The new geometry is orange. It is copied from the locations selected above, and can be modified to give the assets a more specific geometry.'),
'#behaviors' => [
'quick_movement',
],
'#map_settings' => [
'behaviors' => [
'quick_movement' => [
'asset_geometry' => $this->combinedAssetGeometries($prepopulated_assets),
],
],
],
'#display_raw_geometry' => TRUE,
];
// Hidden fields to store asset and location geometry.
$form['asset_geometry_wrapper'] = [
'#type' => 'container',
'#attributes' => [
'id' => 'asset-geometry',
'data-movement-geometry' => 'asset-geometry',
],
'asset_geometry' => [
'#type' => 'hidden',
'#value' => $this->combinedAssetGeometries($this->loadEntityAutocompleteAssets($form_state->getValue('asset'))),
],
];
$form['location_geometry_wrapper'] = [
'#type' => 'container',
'#attributes' => [
'id' => 'location-geometry',
'data-movement-geometry' => 'location-geometry',
],
'location_geometry' => [
'#type' => 'hidden',
'#value' => $this->combinedAssetGeometries($this->loadEntityAutocompleteAssets($form_state->getValue('location'))),
],
];
// Notes.
$form['notes'] = [
'#type' => 'details',
'#title' => $this->t('Notes'),
];
$form['notes']['notes'] = [
'#type' => 'text_format',
'#title' => $this->t('Notes'),
'#title_display' => 'invisible',
'#format' => 'default',
];
// Done.
$form['done'] = [
'#type' => 'checkbox',
'#title' => $this->t('Completed'),
'#default_value' => TRUE,
];
// Attach movement quick form JS.
$form['#attached']['library'][] = 'farm_quick_movement/quick_movement';
return $form;
}
/**
* Ajax callback for asset geometry field.
*/
public function assetGeometryCallback(array $form, FormStateInterface $form_state) {
return $form['asset_geometry_wrapper'];
}
/**
* Ajax callback for location geometry field.
*/
public function locationGeometryCallback(array $form, FormStateInterface $form_state) {
return $form['location_geometry_wrapper'];
}
/**
* Load assets from entity_autocomplete values.
*
* @param array|null $values
* The value from $form_state->getValue().
*
* @return \Drupal\asset\Entity\AssetInterface[]
* Returns an array of assets.
*/
protected function loadEntityAutocompleteAssets($values) {
$entities = [];
if (empty($values)) {
return $entities;
}
foreach ($values as $value) {
if ($value instanceof EntityInterface) {
$entities[] = $value;
}
elseif (!empty($value['target_id'])) {
$entities[] = $this->entityTypeManager->getStorage('asset')->load($value['target_id']);
}
}
return $entities;
}
/**
* Load combined WKT geometry of assets.
*
* @param array $assets
* An array of assets.
*
* @return string
* Returns a WKT geometry string.
*/
protected function combinedAssetGeometries(array $assets) {
if (empty($assets)) {
return '';
}
$geometries = [];
foreach ($assets as $asset) {
$geometries[] = $this->assetLocation->getGeometry($asset);
}
return $this->combineWkt($geometries);
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// Validate that a geometry is only present if a location is specified.
if (empty($form_state->getValue('location')) && !empty($form_state->getValue('geometry'))) {
$form_state->setError($form['geometry'], $this->t('A geometry cannot be set if there is no location.'));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Draft a movement activity log from the user-submitted data.
$timestamp = $form_state->getValue('date')->getTimestamp();
$status = $form_state->getValue('done') ? 'done' : 'pending';
$log = [
'type' => 'activity',
'timestamp' => $timestamp,
'asset' => $form_state->getValue('asset'),
'location' => $form_state->getValue('location'),
'geometry' => $form_state->getValue('geometry'),
'notes' => $form_state->getValue('notes'),
'status' => $status,
'is_movement' => TRUE,
];
// Load assets and locations.
$assets = $this->loadEntityAutocompleteAssets($form_state->getValue('asset'));
$locations = $this->loadEntityAutocompleteAssets($form_state->getValue('location'));
// Generate a name for the log.
$asset_names = $this->entityLabelsSummary($assets);
$location_names = $this->entityLabelsSummary($locations);
$log['name'] = $this->t('Clear location of @assets', ['@assets' => Markup::create($asset_names)]);
if (!empty($location_names)) {
$log['name'] = $this->t('Move @assets to @locations', ['@assets' => Markup::create($asset_names), '@locations' => Markup::create($location_names)]);
}
// Create the log.
$this->createLog($log);
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace Drupal\Tests\farm_quick_movement\Kernel;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Tests\farm_quick\Kernel\QuickFormTestBase;
use Drupal\asset\Entity\Asset;
/**
* Tests for farmOS movement quick form.
*
* @group farm
*/
class QuickMovementTest extends QuickFormTestBase {
/**
* Quick form ID.
*
* @var string
*/
protected $quickFormId = 'movement';
/**
* {@inheritdoc}
*/
protected static $modules = [
'farm_equipment',
'farm_activity',
'farm_land',
'farm_quick_movement',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig([
'farm_activity',
'farm_equipment',
'farm_land',
]);
}
/**
* Test movement quick form submission.
*/
public function testQuickMovement() {
// Get today's date.
$today = new DrupalDateTime('midnight');
// Create two equipment assets and two land assets.
$equipment1 = Asset::create([
'name' => 'Tractor',
'type' => 'equipment',
'status' => 'active',
]);
$equipment1->save();
$equipment2 = Asset::create([
'name' => "Mike's Combine",
'type' => 'equipment',
'status' => 'active',
]);
$equipment2->save();
$location1 = Asset::create([
'name' => 'Field A',
'type' => 'land',
'land_type' => 'field',
'is_fixed' => TRUE,
'is_location' => TRUE,
'intrinsic_geometry' => 'POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))',
'status' => 'active',
]);
$location1->save();
$location2 = Asset::create([
'name' => 'Field B',
'type' => 'land',
'land_type' => 'field',
'is_fixed' => TRUE,
'is_location' => TRUE,
'intrinsic_geometry' => 'POLYGON ((20 40, 40 80, 60 60, 10 20, 20 40))',
'status' => 'active',
]);
$location2->save();
// Programmatically submit the movement quick form.
$form_values = [
'date' => [
'date' => $today->format('Y-m-d'),
'time' => $today->format('H:i:s'),
],
'asset' => [
['target_id' => $equipment1->id()],
['target_id' => $equipment2->id()],
],
'location' => [
['target_id' => $location1->id()],
['target_id' => $location2->id()],
],
'notes' => [
'value' => 'Lorem ipsum',
'format' => 'default',
],
'done' => TRUE,
];
$this->submitQuickForm($form_values);
// Load logs.
$logs = $this->logStorage->loadMultiple();
// Confirm that one log exists.
$this->assertCount(1, $logs);
// Check that the activity log's fields were populated correctly.
$log = $logs[1];
$this->assertEquals('activity', $log->bundle());
$this->assertEquals($today->getTimestamp(), $log->get('timestamp')->value);
$this->assertEquals("Move Tractor, Mike's Combine to Field A, Field B", $log->label());
$this->assertEquals($equipment1->id(), $log->get('asset')->referencedEntities()[0]->id());
$this->assertEquals($equipment2->id(), $log->get('asset')->referencedEntities()[1]->id());
$this->assertEquals($location1->id(), $log->get('location')->referencedEntities()[0]->id());
$this->assertEquals($location2->id(), $log->get('location')->referencedEntities()[1]->id());
$this->assertEquals('Lorem ipsum', $log->get('notes')->value);
$this->assertEquals('GEOMETRYCOLLECTION (POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10)),POLYGON ((20 40, 40 80, 60 60, 10 20, 20 40)))', $log->get('geometry')->value);
$this->assertEquals('done', $log->get('status')->value);
// Programmatically submit the movement quick form again, but this time
// override the geometry.
$form_values['geometry']['value'] = 'POINT (30 10)';
$this->submitQuickForm($form_values);
// Load logs.
$logs = $this->logStorage->loadMultiple();
// Confirm that two logs exist.
$this->assertCount(2, $logs);
// Check that the geometry was overridden.
$log = $logs[2];
$this->assertEquals($form_values['geometry']['value'], $log->get('geometry')->value);
// Programmatically submit the movement quick form again, but this time
// remove the location without removing geometry. This should fail
// validation.
$form_values['location'] = NULL;
$this->submitQuickForm($form_values);
// Load logs.
$logs = $this->logStorage->loadMultiple();
// Confirm that only two logs still exist.
$this->assertCount(2, $logs);
}
}

View File

@@ -0,0 +1,8 @@
name: Planting Quick Form
description: Provides a quick form for recording a planting.
type: module
package: farmOS Quick Forms
core_version_requirement: ^10
dependencies:
- farm:farm_plant
- farm:farm_quick

View File

@@ -0,0 +1,629 @@
<?php
namespace Drupal\farm_quick_planting\Plugin\QuickForm;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\State\StateInterface;
use Drupal\farm_quick\Plugin\QuickForm\QuickFormBase;
use Drupal\farm_quick\Traits\QuickAssetTrait;
use Drupal\farm_quick\Traits\QuickFormElementsTrait;
use Drupal\farm_quick\Traits\QuickLogTrait;
use Drupal\farm_quick\Traits\QuickQuantityTrait;
use Drupal\farm_quick\Traits\QuickStringTrait;
use Drupal\taxonomy\TermInterface;
use Psr\Container\ContainerInterface;
/**
* Planting quick form.
*
* @QuickForm(
* id = "planting",
* label = @Translation("Planting"),
* description = @Translation("Record a planting."),
* helpText = @Translation("This form will create a plant asset, along with optional logs to represent seeding date, harvest date, etc."),
* permissions = {
* "create plant asset",
* }
* )
*
* @internal
*/
class Planting extends QuickFormBase {
use QuickAssetTrait;
use QuickLogTrait;
use QuickQuantityTrait;
use QuickStringTrait;
use QuickFormElementsTrait;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Current user object.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs a QuickFormBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* Current user object.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MessengerInterface $messenger, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, StateInterface $state, AccountInterface $current_user) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $messenger);
$this->messenger = $messenger;
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
$this->state = $state;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('messenger'),
$container->get('entity_type.manager'),
$container->get('module_handler'),
$container->get('state'),
$container->get('current_user'),
);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Load the seasons that were used last time.
$season_ids = $this->state->get('farm.quick.planting.seasons', []);
$seasons = $this->entityTypeManager->getStorage('taxonomy_term')->loadMultiple($season_ids);
// Seasons.
$form['seasons'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Season'),
'#description' => $this->t('What season(s) will this be part of? This is used for organizing assets for future reference, and can be something like "@year" or "@year Summer". This will be prepended to the plant asset name.', ['@year' => date('Y')]),
'#target_type' => 'taxonomy_term',
'#selection_settings' => [
'target_bundles' => ['season'],
],
'#autocreate' => [
'bundle' => 'season',
],
'#tags' => TRUE,
'#default_value' => $seasons,
'#required' => TRUE,
];
// Create a container for crops/varieties.
$form['crops'] = [
'#type' => 'container',
'#tree' => TRUE,
'#attributes' => ['id' => 'plant-crops'],
];
// Create a field for each crop/variety.
$crop_count = $form_state->getValue('crop_count', 1);
for ($i = 0; $i < $crop_count; $i++) {
$counter = $crop_count > 1 ? ' ' . ($i + 1) : '';
$form['crops'][$i] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Crop/variety') . $counter,
'#description' => $this->t("Enter the crop/variety that this is a planting of. As you type, you will have the option of selecting from crops/varieties that you've entered in the past."),
'#target_type' => 'taxonomy_term',
'#selection_settings' => [
'target_bundles' => ['plant_type'],
],
'#autocreate' => [
'bundle' => 'plant_type',
],
'#required' => TRUE,
];
}
// Number of crops/varieties.
$range = range(1, 10);
$form['crop_count'] = [
'#type' => 'select',
'#title' => $this->t('If this is a mix, how many crops/varieties are included?'),
'#options' => array_combine($range, $range),
'#default_value' => 1,
'#ajax' => [
'callback' => [$this, 'plantCropsCallback'],
'wrapper' => 'plant-crops',
],
];
// Create a set of checkboxes to enable log types, based on enabled modules,
// and permission to create them.
$log_type_modules = [
'farm_seeding' => [
'log_type' => 'seeding',
'label' => $this->t('Seeding'),
'default' => TRUE,
],
'farm_transplanting' => [
'log_type' => 'transplanting',
'label' => $this->t('Transplanting'),
],
'farm_harvest' => [
'log_type' => 'harvest',
'label' => $this->t('Harvest'),
],
];
$log_type_options = [];
$log_type_defaults = [];
foreach ($log_type_modules as $module => $option) {
if ($this->moduleHandler->moduleExists($module) && $this->currentUser->hasPermission('create ' . $option['log_type'] . ' log')) {
$log_type_options[$option['log_type']] = $option['label'];
if (!empty($option['default'])) {
$log_type_defaults[$option['log_type']] = $option['log_type'];
}
}
}
if (!empty($log_type_options)) {
$form['log_types'] = [
'#type' => 'checkboxes',
'#title' => $this->t('What events would you like to record?'),
'#options' => $log_type_options,
'#default_value' => $log_type_defaults,
'#ajax' => [
'callback' => [$this, 'plantLogsCallback'],
'wrapper' => 'plant-logs',
],
];
}
// Create a wrapper for logs.
$form['logs_wrapper'] = [
'#type' => 'container',
'#attributes' => ['id' => 'plant-logs'],
];
// Create vertical tabs for logs.
$form['logs_wrapper']['logs'] = [
'#type' => 'vertical_tabs',
];
// Add log forms that can be created for this plant asset.
$enabled_logs = array_filter($form_state->getValue('log_types', $log_type_defaults));
if (in_array('seeding', $enabled_logs)) {
$form['seeding'] = [
'#type' => 'details',
'#title' => $this->t('Seeding'),
'#group' => 'logs',
'#tree' => TRUE,
];
$include_fields = ['date', 'location', 'quantity', 'notes', 'done'];
$quantity_measures = ['count', 'length', 'weight', 'area', 'volume', 'ratio'];
$form['seeding'] += $this->buildLogForm('seeding', $include_fields, $quantity_measures);
}
if (in_array('transplanting', $enabled_logs)) {
$form['transplanting'] = [
'#type' => 'details',
'#title' => $this->t('Transplanting'),
'#group' => 'logs',
'#tree' => TRUE,
];
$include_fields = ['date', 'location', 'quantity', 'notes', 'done'];
$quantity_measures = ['count', 'length', 'weight', 'area', 'volume', 'ratio'];
$form['transplanting'] += $this->buildLogForm('transplanting', $include_fields, $quantity_measures);
}
if (in_array('harvest', $enabled_logs)) {
$form['harvest'] = [
'#type' => 'details',
'#title' => $this->t('Harvest'),
'#group' => 'logs',
'#tree' => TRUE,
];
$include_fields = ['date', 'quantity', 'notes', 'done'];
$form['harvest'] += $this->buildLogForm('harvest', $include_fields);
}
// Plant asset name.
// Provide a checkbox to allow customizing this. Otherwise it will be
// automatically generated on submission.
$form['custom_name'] = [
'#type' => 'checkbox',
'#title' => $this->t('Customize plant asset name'),
'#description' => $this->t('The plant asset name will default to "[Season] [Location] [Crop]" but can be customized if desired.'),
'#default_value' => FALSE,
'#ajax' => [
'callback' => [$this, 'plantNameCallback'],
'wrapper' => 'plant-name',
],
];
$form['name_wrapper'] = [
'#type' => 'container',
'#attributes' => ['id' => 'plant-name'],
];
if ($form_state->getValue('custom_name', FALSE)) {
$form['name_wrapper']['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Plant asset name'),
'#maxlength' => 255,
'#default_value' => $this->generatePlantName($form_state),
'#required' => TRUE,
];
}
return $form;
}
/**
* Build a simplified log form.
*
* @param string $log_type
* The log type.
* @param array $include_fields
* Array of fields to include.
* @param array $quantity_measures
* Array of allowed quantity measures.
*
* @return array
* Returns a Form API array.
*/
protected function buildLogForm(string $log_type, array $include_fields = [], array $quantity_measures = []) {
$form = [];
// Add a hidden value for the log type.
$form['type'] = [
'#type' => 'value',
'#value' => $log_type,
];
// Filter the available quantity measures, if desired.
$quantity_measure_options = quantity_measure_options();
$filtered_quantity_measure_options = $quantity_measure_options;
if (!empty($quantity_measures)) {
$filtered_quantity_measure_options = [];
foreach ($quantity_measures as $measure) {
if (!empty($quantity_measure_options[$measure])) {
$filtered_quantity_measure_options[$measure] = $quantity_measure_options[$measure];
}
}
}
// Create log fields.
$field_info = [];
$field_info['date'] = [
'#type' => 'datetime',
'#title' => $this->t('Date'),
'#default_value' => new DrupalDateTime('midnight', $this->currentUser->getTimeZone()),
'#required' => TRUE,
];
$field_info['done'] = [
'#type' => 'checkbox',
'#title' => $this->t('Completed'),
];
$field_info['location'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Location'),
'#description' => $this->t('Where does this take place?'),
'#target_type' => 'asset',
'#selection_handler' => 'views',
'#selection_settings' => [
'view' => [
'view_name' => 'farm_location_reference',
'display_name' => 'entity_reference',
'arguments' => [],
],
'match_operator' => 'CONTAINS',
],
'#tags' => TRUE,
'#required' => TRUE,
];
$field_info['quantity'] = $this->buildInlineContainer();
$field_info['quantity']['value'] = [
'#type' => 'textfield',
'#title' => $this->t('Quantity'),
'#size' => 16,
];
$field_info['quantity']['units'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Units'),
'#target_type' => 'taxonomy_term',
'#selection_settings' => [
'target_bundles' => ['unit'],
],
'#autocreate' => [
'bundle' => 'unit',
],
'#size' => 16,
];
$field_info['quantity']['measure'] = [
'#type' => 'select',
'#title' => $this->t('Measure'),
'#options' => $filtered_quantity_measure_options,
'#default_value' => 'weight',
];
$field_info['notes'] = [
'#type' => 'text_format',
'#title' => $this->t('Notes'),
'#format' => 'default',
];
foreach ($include_fields as $field) {
if (array_key_exists($field, $field_info)) {
$form[$field] = $field_info[$field];
}
}
return $form;
}
/**
* Generate plant asset name.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
*
* @return string
* Returns a plant asset name string.
*/
protected function generatePlantName(FormStateInterface $form_state) {
// Get the season names.
/** @var \Drupal\taxonomy\TermInterface[] $seasons */
$seasons = $form_state->getValue('seasons') ?? [];
$season_names = [];
foreach ($seasons as $season) {
if (!empty($season['target_id'])) {
$season = $this->entityTypeManager->getStorage('taxonomy_term')->load($season['target_id']);
}
elseif (!empty($season['entity'])) {
$season = $season['entity'];
}
if ($season instanceof TermInterface) {
$season_names[] = $season->label();
}
}
// Get the crop/variety names.
/** @var \Drupal\taxonomy\TermInterface[] $crops */
$crops = $form_state->getValue('crops') ?? [];
$crop_names = [];
foreach ($crops as $crop) {
if (is_numeric($crop)) {
$crop = $this->entityTypeManager->getStorage('taxonomy_term')->load($crop);
}
elseif (!empty($crop['entity'])) {
$crop = $crop['entity'];
}
if ($crop instanceof TermInterface) {
$crop_names[] = $crop->label();
}
}
// Get the location name(s).
// The "final" location of the plant is assumed to be the transplanting
// location (if the transplanting module is enabled). If a transplanting is
// not being created, but a seeding is, then use the seeding location.
$location_keys = [
['seeding', 'location'],
['transplanting', 'location'],
];
$location_name = '';
foreach ($location_keys as $key) {
if ($form_state->hasValue($key)) {
$location_names = array_map(function ($value) {
return $this->entityTypeManager->getStorage('asset')->load($value['target_id'])->label();
}, $form_state->getValue($key));
$location_name = implode(', ', $location_names);
}
}
// Generate the plant name, giving priority to the seasons and crops.
$name_parts = [
'seasons' => implode('/', $season_names),
'location' => $location_name,
'crops' => implode(', ', $crop_names),
];
$priority_keys = ['seasons', 'crops'];
return $this->prioritizedString($name_parts, $priority_keys);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// If a custom plant name was provided, use that. Otherwise generate one.
$plant_name = $this->generatePlantName($form_state);
if (!empty($form_state->getValue('custom_name', FALSE)) && $form_state->hasValue('name')) {
$plant_name = $form_state->getValue('name');
}
// Create a new planting asset.
$plant_asset = $this->createAsset([
'type' => 'plant',
'name' => $plant_name,
'plant_type' => $form_state->getValue('crops'),
'season' => $form_state->getValue('seasons'),
]);
// Remember the selected seasons for future reference.
$season_ids = [];
foreach ($plant_asset->get('season')->referencedEntities() as $entity) {
$season_ids[] = $entity->id();
}
if (!empty($season_ids)) {
$this->state->set('farm.quick.planting.seasons', $season_ids);
}
// Generate logs.
$log_types = [
'seeding',
'transplanting',
'harvest',
];
foreach ($log_types as $log_type) {
// If there are no values for this log type, skip it.
if (!$form_state->hasValue($log_type)) {
continue;
}
// Get the log values.
$log_values = $form_state->getValue($log_type);
// Name the log based on the type and asset.
switch ($log_type) {
case 'seeding':
$log_name = $this->t('Seed @asset', ['@asset' => Markup::create($plant_asset->label())]);
break;
case 'transplanting':
$log_name = $this->t('Transplant @asset', ['@asset' => Markup::create($plant_asset->label())]);
break;
case 'harvest':
$log_name = $this->t('Harvest @asset', ['@asset' => Markup::create($plant_asset->label())]);
break;
}
// If the log is a seeding or transplanting, it is a movement.
$is_movement = FALSE;
if (in_array($log_type, ['seeding', 'transplanting'])) {
$is_movement = TRUE;
}
// Set the log status.
$status = 'pending';
if (!empty($log_values['done'])) {
$status = 'done';
}
// Create the log.
$this->createLog([
'type' => $log_type,
'name' => $log_name,
'timestamp' => $log_values['date']->getTimestamp(),
'asset' => $plant_asset,
'quantity' => [$this->prepareQuantity($log_values['quantity'])],
'location' => $log_values['location'] ?? NULL,
'is_movement' => $is_movement,
'notes' => $log_values['notes'] ?? NULL,
'status' => $status,
]);
}
}
/**
* Prepare quantity values for use with createLog() or createQuantity().
*
* @param array $values
* Quantity field values from the form.
*
* @return array|null
* Returns an array for createQuantity() or NULL if no quantity value.
*/
protected function prepareQuantity(array $values) {
// If there is no value, return an empty array.
if (empty($values['value'])) {
return NULL;
}
// If units is specified, then we need to convert it to units_id, which
// is expected by createLog() and createQuantity().
if (!empty($values['units'])) {
// If units is a numeric value, assume that it is already a term ID.
// This will be the case when the form value is set programatically
// (eg: via automated tests).
if (is_numeric($values['units'])) {
$values['units_id'] = $values['units'];
unset($values['units']);
}
// Or, if units is an array, and it has either a target_id or entity,
// translate it to units_id. This will be the case when a term is selected
// via the UI, when referencing an existing term or creating a new one,
// respectively.
elseif (is_array($values['units'])) {
// If an existing term is selected, target_id will be set.
if (!empty($values['units']['target_id'])) {
$values['units_id'] = $values['units']['target_id'];
unset($values['units']);
}
// Or, if a new term is being created, the full entity is available.
elseif (!empty($values['units']['entity']) && $values['units']['entity'] instanceof TermInterface) {
$values['units'] = $values['units']['entity'];
}
}
}
// Return the prepared values.
return $values;
}
/**
* Ajax callback for crop/variety fields.
*/
public function plantCropsCallback(array $form, FormStateInterface $form_state) {
return $form['crops'];
}
/**
* Ajax callback for logs fields.
*/
public function plantLogsCallback(array $form, FormStateInterface $form_state) {
return $form['logs_wrapper'];
}
/**
* Ajax callback for plant name field.
*/
public function plantNameCallback(array $form, FormStateInterface $form_state) {
return $form['name_wrapper'];
}
}

View File

@@ -0,0 +1,332 @@
<?php
namespace Drupal\Tests\farm_quick_planting\Kernel;
use Drupal\Tests\farm_quick\Kernel\QuickFormTestBase;
use Drupal\asset\Entity\Asset;
use Drupal\taxonomy\Entity\Term;
/**
* Tests for farmOS planting quick form.
*
* @group farm
*/
class QuickPlantingTest extends QuickFormTestBase {
/**
* Quick form ID.
*
* @var string
*/
protected $quickFormId = 'planting';
/**
* {@inheritdoc}
*/
protected static $modules = [
'entity_reference_validators',
'farm_harvest',
'farm_land',
'farm_plant',
'farm_plant_type',
'farm_quantity_standard',
'farm_quick_planting',
'farm_season',
'farm_seeding',
'farm_transplanting',
'farm_unit',
'field',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig([
'farm_harvest',
'farm_land',
'farm_plant',
'farm_plant_type',
'farm_quantity_standard',
'farm_seeding',
'farm_transplanting',
'system',
]);
}
/**
* Test simple planting quick form submission.
*/
public function testQuickPlantingSimple() {
// Create a season and crop to reference.
$season = Term::create([
'name' => '2022',
'vid' => 'season',
]);
$season->save();
$crop = Term::create([
'name' => "Jacob's Cattle Bean",
'vid' => 'plant_type',
]);
$crop->save();
// Submit the planting quick form.
$this->submitQuickForm([
'seasons' => [['target_id' => $season->id()]],
'crops' => [[['target_id' => $crop->id()]]],
'crop_count' => 1,
'log_types' => [],
]);
// Confirm that one asset was created.
$assets = $this->assetStorage->loadMultiple();
$this->assertCount(1, $assets);
// Check that the asset's fields were populated correctly.
$asset = $assets[1];
$this->assertEquals('plant', $asset->bundle());
$this->assertEquals("2022 Jacob's Cattle Bean", $asset->label());
$this->assertEquals('active', $asset->get('status')->value);
$this->assertEquals($season->id(), $asset->get('season')->referencedEntities()[0]->id());
$this->assertEquals($crop->id(), $asset->get('plant_type')->referencedEntities()[0]->id());
// Test with multiple crops.
$crop1 = Term::create([
'name' => 'Winter rye',
'vid' => 'plant_type',
]);
$crop1->save();
$crop2 = Term::create([
'name' => 'Vetch',
'vid' => 'plant_type',
]);
$crop2->save();
// Submit the planting quick form.
$this->submitQuickForm([
'seasons' => [['target_id' => $season->id()]],
'crops' => [
[['target_id' => $crop1->id()]],
[['target_id' => $crop2->id()]],
],
'crop_count' => 2,
'log_types' => [],
]);
// Confirm that a second asset was created.
$assets = $this->assetStorage->loadMultiple();
$this->assertCount(2, $assets);
// Check that the asset has multiple crops and is named correctly.
$asset = $assets[2];
$this->assertEquals('2022 Winter rye, Vetch', $asset->label());
$this->assertEquals($crop1->id(), $asset->get('plant_type')->referencedEntities()[0]->id());
$this->assertEquals($crop2->id(), $asset->get('plant_type')->referencedEntities()[1]->id());
// Test overriding the plant name.
$custom_name = "Jacob's Cattle Bean of the 2022 season";
$this->submitQuickForm([
'seasons' => [['target_id' => $season->id()]],
'crops' => [
[['target_id' => $crop->id()]],
],
'crop_count' => 1,
'log_types' => [],
'custom_name' => TRUE,
'name' => $custom_name,
]);
// Confirm that a third asset was created.
$assets = $this->assetStorage->loadMultiple();
$this->assertCount(3, $assets);
// Check that the asset name was overridden.
$asset = $assets[3];
$this->assertEquals($custom_name, $asset->label());
}
/**
* Test planting with logs.
*/
public function testQuickPlantingLogs() {
// Create a season, crop, and two land assets to reference.
$season = Term::create([
'name' => '2022',
'vid' => 'season',
]);
$season->save();
$crop = Term::create([
'name' => 'Lettuce',
'vid' => 'plant_type',
]);
$crop->save();
$land1 = Asset::create([
'name' => 'Field A',
'type' => 'land',
'land_type' => 'field',
'is_fixed' => TRUE,
'is_location' => TRUE,
'status' => 'active',
]);
$land1->save();
$land2 = Asset::create([
'name' => 'Field B',
'type' => 'land',
'land_type' => 'field',
'is_fixed' => TRUE,
'is_location' => TRUE,
'status' => 'active',
]);
$land2->save();
// Programmatically submit the planting quick form.
$this->submitQuickForm([
'seasons' => [['target_id' => $season->id()]],
'crops' => [[['target_id' => $crop->id()]]],
'crop_count' => 1,
'log_types' => [
'seeding' => 'seeding',
],
'seeding' => [
'type' => 'seeding',
'date' => [
'date' => '2022-05-15',
'time' => '00:00:00',
],
'location' => [
['target_id' => $land1->id()],
],
'quantity' => [
'measure' => 'weight',
'value' => '10.01',
'units' => 'kg',
],
'notes' => [
'value' => 'Lorem ipsum',
'format' => 'default',
],
'done' => TRUE,
],
]);
// Load assets and logs.
$assets = $this->assetStorage->loadMultiple();
$logs = $this->logStorage->loadMultiple();
// Confirm that three assets (land + plant) and one log exists.
$this->assertCount(3, $assets);
$this->assertCount(1, $logs);
// Check that the asset name includes the seeding location.
$asset = $assets[3];
$this->assertEquals('2022 Field A Lettuce', $asset->label());
// Check that the seeding log's fields were populated correctly.
$log = $logs[1];
$this->assertEquals('seeding', $log->bundle());
$this->assertEquals('Seed ' . $asset->label(), $log->label());
$this->assertEquals(strtotime('2022-05-15'), $log->get('timestamp')->value);
$this->assertEquals($asset->id(), $log->get('asset')->referencedEntities()[0]->id());
$this->assertEquals($land1->id(), $log->get('location')->referencedEntities()[0]->id());
$this->assertEquals('weight', $log->get('quantity')->referencedEntities()[0]->get('measure')->value);
$this->assertEquals('10.01', $log->get('quantity')->referencedEntities()[0]->get('value')[0]->get('decimal')->getValue());
$this->assertEquals('kg', $log->get('quantity')->referencedEntities()[0]->get('units')->referencedEntities()[0]->get('name')->value);
$this->assertEquals('Lorem ipsum', $log->get('notes')->value);
$this->assertEquals('done', $log->get('status')->value);
// Test creating multiple logs.
$this->submitQuickForm([
'seasons' => [['target_id' => $season->id()]],
'crops' => [[['target_id' => $crop->id()]]],
'crop_count' => 1,
'log_types' => [
'seeding' => 'seeding',
'transplanting' => 'transplanting',
'harvest' => 'harvest',
],
'seeding' => [
'type' => 'seeding',
'date' => [
'date' => '2022-05-15',
'time' => '00:00:00',
],
'location' => [
['target_id' => $land1->id()],
],
'notes' => [],
'done' => TRUE,
],
'transplanting' => [
'type' => 'transplanting',
'date' => [
'date' => '2022-06-15',
'time' => '00:00:00',
],
'location' => [
['target_id' => $land2->id()],
],
'notes' => [],
'done' => FALSE,
],
'harvest' => [
'type' => 'harvest',
'date' => [
'date' => '2022-07-15',
'time' => '00:00:00',
],
'notes' => [],
'done' => FALSE,
],
]);
// Confirm that another asset and 3 more logs were created.
$assets = $this->assetStorage->loadMultiple();
$logs = $this->logStorage->loadMultiple();
$this->assertCount(4, $assets);
$this->assertCount(4, $logs);
// Check that the asset name includes the transplanting location.
$asset = $assets[4];
$this->assertEquals('2022 Field B Lettuce', $asset->label());
// Check that the transplanting and harvest logs are pending.
$log = $logs[3];
$this->assertEquals('pending', $log->get('status')->value);
$log = $logs[4];
$this->assertEquals('pending', $log->get('status')->value);
// Test referencing multiple locations.
$this->submitQuickForm([
'seasons' => [['target_id' => $season->id()]],
'crops' => [[['target_id' => $crop->id()]]],
'crop_count' => 1,
'log_types' => [
'seeding' => 'seeding',
],
'seeding' => [
'type' => 'seeding',
'date' => [
'date' => '2022-05-15',
'time' => '00:00:00',
],
'location' => [
['target_id' => $land1->id()],
['target_id' => $land2->id()],
],
'notes' => [],
'done' => TRUE,
],
]);
// Confirm that another log was created and it references both locations.
$logs = $this->logStorage->loadMultiple();
$this->assertCount(5, $logs);
$log = $logs[5];
$this->assertEquals($land1->id(), $log->get('location')->referencedEntities()[0]->id());
$this->assertEquals($land2->id(), $log->get('location')->referencedEntities()[1]->id());
}
}