commerce_shipping-8.x-2.0-rc2/src/Form/ShipmentForm.php
src/Form/ShipmentForm.php
<?php
namespace Drupal\commerce_shipping\Form;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\ContentEntityForm;
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\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;
/**
* The shipment entity.
*
* @var \Drupal\commerce_shipping\Entity\ShipmentInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = parent::create($container);
$instance->packageTypeManager = $container->get('plugin.manager.commerce_package_type');
return $instance;
}
/**
* {@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(intval($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']),
array_merge($form['#parents'], ['shipment_items']),
],
'#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']);
/** @var \Drupal\commerce_shipping\Entity\ShipmentInterface $shipment */
$shipment = $this->entity;
if ($recalculate) {
$form_state->set('recalculate_shipping', TRUE);
$title = $form_state->getValue('title');
if (!empty($title)) {
$shipment->setTitle($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 $shipment;
}
/** @var \Drupal\commerce_shipping\Plugin\Commerce\PackageType\PackageTypeInterface $package_type */
$package_type = $this->packageTypeManager->createInstance($form_state->getValue('package_type'));
$shipment->setPackageType($package_type);
}
return $shipment;
}
/**
* {@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);
$return = $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()) {
if ($order->getState()->getId() == 'draft') {
$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 @label.', ['@label' => $order->label()]));
$form_state->setRedirect('entity.commerce_shipment.collection', ['commerce_order' => $order->id()]);
return $return;
}
/**
* 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);
}
}
}
