commerce_shipping-8.x-2.0-rc2/src/Form/ShipmentForm.php
src/Form/ShipmentForm.php
<?php namespace Drupal\commerce_shipping\Form; use Drupal\Component\Datetime\TimeInterface; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\ContentEntityForm; use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\commerce\AjaxFormTrait; use Drupal\commerce_order\Entity\OrderInterface; use Drupal\commerce_order\Entity\OrderItemInterface; use Drupal\commerce_shipping\PackageTypeManagerInterface; use Drupal\commerce_shipping\ShipmentItem; use Drupal\physical\Weight; use Drupal\physical\WeightUnit; use Drupal\profile\Entity\ProfileInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Defines the shipment add/edit form. */ class ShipmentForm extends ContentEntityForm { use AjaxFormTrait; /** * The package type manager. * * @var \Drupal\commerce_shipping\PackageTypeManagerInterface */ protected $packageTypeManager; /** * Constructs a new ShipmentForm object. * * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository * The entity repository. * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info * The entity type bundle info. * @param \Drupal\Component\Datetime\TimeInterface $time * The time. * @param \Drupal\commerce_shipping\PackageTypeManagerInterface $package_type_manager * The package type manager. */ public function __construct(EntityRepositoryInterface $entity_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info, TimeInterface $time, PackageTypeManagerInterface $package_type_manager) { parent::__construct($entity_repository, $entity_type_bundle_info, $time); $this->packageTypeManager = $package_type_manager; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( $container->get('entity.repository'), $container->get('entity_type.bundle.info'), $container->get('datetime.time'), $container->get('plugin.manager.commerce_package_type') ); } /** * {@inheritdoc} */ public function form(array $form, FormStateInterface $form_state) { // Workaround for core bug #2897377. $form['#id'] = Html::getId($form_state->getBuildInfo()['form_id']); /** @var \Drupal\commerce_shipping\Entity\ShipmentInterface $shipment */ $shipment = $this->entity; $order_id = $shipment->get('order_id')->target_id; if (!$order_id) { $order_id = $this->getRouteMatch()->getRawParameter('commerce_order'); $shipment->set('order_id', $order_id); } /** @var \Drupal\commerce_order\Entity\OrderInterface $order */ $order = $shipment->getOrder(); /** @var \Drupal\profile\Entity\ProfileInterface $shipping_profile */ $shipping_profile = $shipment->getShippingProfile(); if (!$shipping_profile) { /** @var \Drupal\commerce_shipping\Entity\ShipmentTypeInterface $shipment_type */ $shipment_type = $this->entityTypeManager->getStorage('commerce_shipment_type')->load($shipment->bundle()); /** @var \Drupal\profile\Entity\ProfileInterface $shipping_profile */ $shipping_profile = $this->entityTypeManager->getStorage('profile')->create([ 'type' => $shipment_type->getProfileTypeId(), 'uid' => 0, ]); $address = [ '#type' => 'address', '#default_value' => [], ]; $shipping_profile->set('address', $address); $shipment->setShippingProfile($shipping_profile); } // Store the original amount for ShipmentForm::save(). $form_state->set('original_amount', $shipment->getAmount()); $form = parent::form($form, $form_state); // The ShippingProfileWidget doesn't output a fieldset because that makes // sense in a checkout context, but on the admin form it is clearer for // profile fields to be visually grouped. $form['shipping_profile']['widget'][0]['#type'] = 'fieldset'; // Fixes illegal choice has been detected message upon AJAX reload. if (empty($form['shipping_method']['widget'][0]['#options'])) { $form['shipping_method']['#access'] = FALSE; } // Ensure selecting a different address refreshes the entire form. if (isset($form['shipping_profile']['widget'][0]['profile']['select_address'])) { $form['shipping_profile']['widget'][0]['profile']['select_address']['#ajax'] = [ 'callback' => [get_class($this), 'ajaxRefreshForm'], ]; // Selecting a different address should trigger a recalculation. $form['shipping_profile']['widget'][0]['profile']['select_address']['#recalculate'] = TRUE; } // Prepopulate the title on shipments that have no title. $existing_shipments = count($order->get('shipments')->referencedEntities()); $auto_title = $this->t('Shipment #@number', ['@number' => ($existing_shipments + 1)]); $form['title']['widget'][0]['value']['#default_value'] = $shipment->getTitle() ?? $auto_title; $package_types = $this->packageTypeManager->getDefinitions(); $package_type_options = []; foreach ($package_types as $package_type) { $unit = ' ' . array_pop($package_type['dimensions']); $dimensions = ' (' . implode(' x ', $package_type['dimensions']) . $unit . ')'; $package_type_options[$package_type['id']] = $package_type['label'] . $dimensions; } $package_type = $shipment->getPackageType(); $form['package_type'] = [ '#type' => 'select', '#title' => $this->t('Package Type'), '#options' => $package_type_options, '#default_value' => $package_type ? $package_type->getId() : '', '#access' => count($package_types) > 1, ]; $order_items = $order->getItems(); $order_item_ids = array_map(fn (OrderItemInterface $order_item) => $order_item->id(), $order_items); /** @var \Drupal\commerce_shipping\ShipmentStorageInterface $shipment_storage */ $shipment_storage = $this->entityTypeManager->getStorage('commerce_shipment'); // Get all of the shipments for the current order. $order_shipments = $shipment_storage->loadMultipleByOrder($order); // Store order_items that are already tied to shipments on this order. $already_on_shipment = []; foreach ($order_shipments as $order_shipment) { if ($order_shipment->id() != $shipment->id()) { $shipment_items = $order_shipment->getItems(); foreach ($shipment_items as $shipment_item) { $order_item_id = $shipment_item->getOrderItemId(); $already_on_shipment[$order_item_id] = $order_item_id; } } } $shipment_item_options = []; // Populates the default values by looking at the items already in this // shipment. $shipment_item_defaults = []; $shipment_items = $shipment->getItems(); /** @var \Drupal\commerce_shipping\ShipmentItem $shipment_item */ foreach ($shipment_items as $shipment_item) { $shipment_item_id = $shipment_item->getOrderItemId(); if (!in_array($shipment_item_id, $order_item_ids)) { // The order item was deleted on the order. continue; } $shipment_item_defaults[$shipment_item_id] = $shipment_item_id; $shipment_item_options[$shipment_item_id] = $shipment_item->getTitle(); } /** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */ foreach ($order_items as $order_item) { // Skip shipment items that are already on this shipment. if (isset($shipment_item_options[$order_item->id()]) || !$order_item->hasField('purchased_entity') || in_array($order_item->id(), $already_on_shipment, TRUE)) { continue; } // Only allow items that aren't already on a shipment // have a purchasable entity and implement the shippable trait. $purchasable_entity = $order_item->getPurchasedEntity(); if (!empty($purchasable_entity) && $purchasable_entity->hasField('weight')) { $shipment_item_options[$order_item->id()] = $order_item->label(); } } $form['shipment_items'] = [ '#type' => 'checkboxes', '#title' => $this->t('Shipment items'), '#options' => $shipment_item_options, '#default_value' => $shipment_item_defaults, '#required' => TRUE, '#weight' => 48, ]; $form['recalculate_shipping'] = [ '#type' => 'button', '#value' => $this->t('Recalculate shipping'), '#recalculate' => TRUE, '#ajax' => [ 'callback' => [get_class($this), 'ajaxRefreshForm'], ], // The calculation process only needs a valid shipping profile. '#limit_validation_errors' => [ array_merge($form['#parents'], ['shipping_profile']), ], '#weight' => 49, '#after_build' => [ [static::class, 'clearValues'], ], ]; return $form; } /** * Ajax callback. */ public static function ajaxRefresh(array $form, FormStateInterface $form_state) { $triggering_element = $form_state->getTriggeringElement(); $parents = array_slice($triggering_element['#parents'], 0, -1); return NestedArray::getValue($form, $parents); } /** * Clears user input of selected shipping rates if recalculation occurred. * * This is required to prevent invalid options being selected is a shipping * rate is no longer available. * * @param array $element * The element. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state. * * @return array * The element. */ public static function clearValues(array $element, FormStateInterface $form_state) { $triggering_element = $form_state->getTriggeringElement(); if (!$triggering_element) { return $element; } $triggering_element_name = end($triggering_element['#parents']); if (in_array($triggering_element_name, ['recalculate_shipping', 'select_address'], TRUE)) { $user_input = &$form_state->getUserInput(); unset($user_input['shipping_method']); } return $element; } /** * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state) { parent::validateForm($form, $form_state); $triggering_element = $form_state->getTriggeringElement(); $recalculate = !empty($triggering_element['#recalculate']); if ($recalculate) { $form_state->set('recalculate_shipping', TRUE); /** @var \Drupal\commerce_shipping\Entity\ShipmentInterface $shipment */ $shipment = $this->entity; $shipment->setTitle($form_state->getValue('title')); $base_form_key = ['shipping_profile', '0', 'profile']; $selected_profile_key = array_merge($base_form_key, ['select_address']); $selected_profile_id = $form_state->getValue($selected_profile_key); $address_key = array_merge($base_form_key, ['address', '0', 'address']); $address = $form_state->getValue($address_key); // If the address entry form is open, copy the address into the shipping profile so // rates can be returned. If a new address is being entered the shipment profile will // be emptied so no rates are returned. if ($address !== NULL || $selected_profile_id === '_new') { $shipment->getShippingProfile()->set('address', $address); } // If a different profile was selected, load it and use its address. elseif (!empty($selected_profile_id) && is_numeric($selected_profile_id)) { $profile_storage = $this->entityTypeManager->getStorage('profile'); $selected_profile = $profile_storage->load($selected_profile_id); assert($selected_profile instanceof ProfileInterface); $shipment->getShippingProfile()->set('address', $selected_profile->get('address')->first()->toArray()); } // Add the shipping items. $this->addShippingItems($form, $form_state); if (empty($form_state->getValue('package_type'))) { return; } $package_type = $this->packageTypeManager->createInstance($form_state->getValue('package_type')); $shipment->setPackageType($package_type); } } /** * {@inheritdoc} */ public function save(array $form, FormStateInterface $form_state) { /** @var \Drupal\commerce_shipping\Entity\ShipmentInterface $shipment */ $shipment = $this->getEntity(); $this->addShippingItems($form, $form_state); $shipment->setData('owned_by_packer', FALSE); $shipment->save(); // Make sure the shipment gets added to the order. $order = $shipment->getOrder(); $order_shipments = $order->get('shipments'); $shipment_exists = FALSE; $save_order = FALSE; // Loop over the order shipments to make sure this // shipment exists. foreach ($order_shipments->getValue() as $order_shipment) { if ($order_shipment['target_id'] == $shipment->id()) { $shipment_exists = TRUE; } } // Check if the shipment amount has changed, if so we need to trigger // an order refresh so that the shipping adjustment gets adjusted. if ($form_state->get('original_amount') != $shipment->getAmount()) { $order->setRefreshState(OrderInterface::REFRESH_ON_SAVE); $save_order = TRUE; } // Add the shipment to the order if it doesn't exist. if (!$shipment_exists) { $order_shipments->appendItem($shipment); $save_order = TRUE; } // Save the parent order if the shipment amount has changed or if the // shipment was appended to the order. if ($save_order) { $order->save(); } $this->messenger()->addMessage($this->t('Saved shipment for order @order.', ['@order' => $order->getOrderNumber()])); $form_state->setRedirect('entity.commerce_shipment.collection', ['commerce_order' => $order->id()]); } /** * Creates new shipping items from the form and adds them to the shipment. * * @param array $form * A nested array of form elements comprising the form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. */ protected function addShippingItems(array &$form, FormStateInterface $form_state) { /** @var \Drupal\commerce_shipping\Entity\ShipmentInterface $shipment */ $shipment = $this->entity; // Clear the shipping items to make sure the list is fresh when we add them. $shipment->setItems([]); /** @var \Drupal\commerce_shipping\ShipmentItem $shipment_item */ foreach ($form_state->getValue('shipment_items') as $key => $value) { if ($value == 0) { // The item was not included in the shipment. continue; } /** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */ $order_item = $this->entityTypeManager->getStorage('commerce_order_item')->load($key); if (!$order_item) { // The order item was deleted on the order. continue; } $quantity = $order_item->getQuantity(); $purchased_entity = $order_item->getPurchasedEntity(); if ($purchased_entity->get('weight')->isEmpty()) { $weight = new Weight(1, WeightUnit::GRAM); } else { /** @var \Drupal\physical\Plugin\Field\FieldType\MeasurementItem $weight_item */ $weight_item = $purchased_entity->get('weight')->first(); $weight = $weight_item->toMeasurement(); } $shipment_item = new ShipmentItem([ 'order_item_id' => $order_item->id(), 'title' => $purchased_entity->label(), 'quantity' => $quantity, 'weight' => $weight->multiply($quantity), 'declared_value' => $order_item->getTotalPrice(), ]); $shipment->addItem($shipment_item); } } }