diff -u b/config/schema/commerce_pos.schema.yml b/config/schema/commerce_pos.schema.yml --- b/config/schema/commerce_pos.schema.yml +++ b/config/schema/commerce_pos.schema.yml @@ -5,9 +5,6 @@ num_results: type: integer label: 'Number of results to display' - order_item_view_mode: - type: string - label: 'Order item view mode' placeholder: type: label label: 'Placeholder' diff -u b/src/Plugin/Field/FieldWidget/PosOrderItemWidget.php b/src/Plugin/Field/FieldWidget/PosOrderItemWidget.php --- b/src/Plugin/Field/FieldWidget/PosOrderItemWidget.php +++ b/src/Plugin/Field/FieldWidget/PosOrderItemWidget.php @@ -3,6 +3,7 @@ namespace Drupal\commerce_pos\Plugin\Field\FieldWidget; use Drupal\commerce_order\Entity\OrderItem; +use Drupal\commerce_price\Price; use Drupal\commerce_product\Entity\ProductVariation; use Drupal\Component\Utility\Html; use Drupal\Core\Entity\EntityDisplayRepositoryInterface; @@ -93,7 +94,6 @@ 'placeholder' => 'Enter a product name', 'num_results' => 10, 'purchasable_entity_view_mode' => 'commerce_pos_product_select', - 'order_item_view_mode' => 'commerce_pos_order_item', ] + parent::defaultSettings(); } @@ -105,20 +105,20 @@ $elements['size'] = [ '#type' => 'number', - '#title' => t('Size of textfield'), + '#title' => $this->t('Size of textfield'), '#default_value' => $this->getSetting('size'), '#required' => TRUE, '#min' => 1, ]; $elements['placeholder'] = [ '#type' => 'textfield', - '#title' => t('Placeholder'), + '#title' => $this->t('Placeholder'), '#default_value' => $this->getSetting('placeholder'), - '#description' => t('Text that will be shown inside the field until a value is entered. This hint is usually a sample value or a brief description of the expected format.'), + '#description' => $this->t('Text that will be shown inside the field until a value is entered. This hint is usually a sample value or a brief description of the expected format.'), ]; $elements['num_results'] = [ '#type' => 'number', - '#title' => t('Number of search results'), + '#title' => $this->t('Number of search results'), '#default_value' => $this->getSetting('num_results'), '#required' => TRUE, '#min' => 1, @@ -126,22 +126,13 @@ ]; $elements['purchasable_entity_view_mode'] = [ '#type' => 'select', - '#title' => t('Purchasable entity view mode'), + '#title' => $this->t('Purchasable entity view mode'), '#default_value' => $this->getSetting('purchasable_entity_view_mode'), '#required' => TRUE, // @todo use the "Purchasable entity type" from the order item type. '#options' => $this->entityDisplayRepository->getViewModeOptions('commerce_product_variation'), '#description' => 'This view mode is used for the list of product variations to select from.', ]; - $elements['order_item_view_mode'] = [ - '#type' => 'select', - '#title' => t('Order item view mode'), - '#default_value' => $this->getSetting('order_item_view_mode'), - '#required' => TRUE, - // @todo use the entity type from the reference field. - '#options' => $this->entityDisplayRepository->getViewModeOptions('commerce_order_item'), - '#description' => 'This view mode is used list the order items on the form.', - ]; return $elements; } @@ -152,14 +143,13 @@ public function settingsSummary() { $summary = []; - $summary[] = t('Textfield size: @size', ['@size' => $this->getSetting('size')]); + $summary[] = $this->t('Textfield size: @size', ['@size' => $this->getSetting('size')]); if (!empty($this->getSetting('placeholder'))) { - $summary[] = t('Placeholder: @placeholder', ['@placeholder' => $this->getSetting('placeholder')]); + $summary[] = $this->t('Placeholder: @placeholder', ['@placeholder' => $this->getSetting('placeholder')]); } - $summary[] = t('Number of results: @num_results', ['@num_results' => $this->getSetting('num_results')]); - $summary[] = t('Purchasable entity view mode: @view_mode', ['@view_mode' => $this->getSetting('purchasable_entity_view_mode')]); - $summary[] = t('Order item view mode: @view_mode', ['@view_mode' => $this->getSetting('order_item_view_mode')]); + $summary[] = $this->t('Number of results: @num_results', ['@num_results' => $this->getSetting('num_results')]); + $summary[] = $this->t('Purchasable entity view mode: @view_mode', ['@view_mode' => $this->getSetting('purchasable_entity_view_mode')]); return $summary; } @@ -214,14 +204,78 @@ // Render a list of products that have been added to the order. $referenced_entities = $items->referencedEntities(); - $element['products'] = []; + $element['order_items'] = [ + '#type' => 'table', + '#header' => [ + $this->t('Product'), + $this->t('Unit price'), + $this->t('Quantity'), + '', + ], + ]; /** @var \Drupal\commerce_order\Entity\OrderItem $entity */ foreach ($referenced_entities as $entity) { - $product = $entity->getPurchasedEntity(); - $view_builder = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId()); - $element['products'][]['product'] = [ - $view_builder->view($entity, $this->getSetting('order_item_view_mode'), $entity->language()->getId()), - [ + $element['order_items'][] = $this->orderItemForm($entity, $wrapper_id); + } + + $element['#default_value'] = $items->referencedEntities(); + return ['target_id' => $element]; + } + + /** + * Creates a form for an order item. + * + * @param \Drupal\commerce_order\Entity\OrderItem $order_item + * The prder item. + * @param string $wrapper_id + * The wrapper ID to use for Ajax. + * + * @return array + * The oprder item form. + */ + protected function orderItemForm(OrderItem $order_item, $wrapper_id) { + $product = $order_item->getPurchasedEntity(); + // If we don't render product here the title appears in the wrong place. + // @todo work out why are fix this. + $view_builder = $this->entityTypeManager->getViewBuilder($order_item->getEntityTypeId()); + $product_render = $view_builder->view($product, $this->getSetting('purchasable_entity_view_mode'), $order_item->language()->getId()); + $form = [ + 'purchasable_entity' => [ + '#type' => 'markup', + '#markup' => \Drupal::service('renderer')->render($product_render), + ], + 'unit_price' => [ + '#type' => 'commerce_price', + '#title' => $this->t('Unit price'), + '#title_display' => 'invisible', + '#name' => 'update_unit_price_' . $product->id(), + '#default_value' => $order_item->getUnitPrice()->toArray(), + '#allow_negative' => TRUE, + '#order_item_id' => $order_item->id(), + '#ajax' => [ + 'callback' => [$this, 'ajaxRefresh'], + 'wrapper' => $wrapper_id, + 'event' => 'change', + ], + // @todo Remove once commerce_price fields support AJAX, see + // https://www.drupal.org/project/commerce/issues/2923388. + '#disabled' => TRUE, + ], + 'quantity' => [ + '#title' => $this->t('Quantity'), + '#title_display' => 'invisible', + '#type' => 'number', + '#default_value' => $order_item->getQuantity(), + '#ajax' => [ + 'callback' => [$this, 'ajaxRefresh'], + 'wrapper' => $wrapper_id, + 'event' => 'change', + ], + '#order_item_id' => $order_item->id(), + ], + 'adjust_quantity' => [ + '#type' => 'container', + 'adjust_order_add' => [ '#type' => 'button', '#name' => 'adjust_order_add_' . $product->id(), '#value' => '+', @@ -230,10 +284,10 @@ 'wrapper' => $wrapper_id, ], '#product_id' => $product->id(), - '#order_item_id' => $entity->id(), + '#order_item_id' => $order_item->id(), '#quantity' => 1, ], - [ + 'adjust_order_remove' => [ '#type' => 'button', '#value' => '-', '#name' => 'adjust_order_remove_' . $product->id(), @@ -242,15 +296,12 @@ 'wrapper' => $wrapper_id, ], '#product_id' => $product->id(), - '#order_item_id' => $entity->id(), + '#order_item_id' => $order_item->id(), '#quantity' => -1, ], - - ]; - } - - $element['#default_value'] = $items->referencedEntities(); - return ['target_id' => $element]; + ], + ]; + return $form; } /** @@ -272,16 +323,33 @@ */ protected function processFormSubmission(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) { $trigger_element = $form_state->getTriggeringElement(); + $order = FALSE; if ($trigger_element['#name'] === 'order_items[target_id][product_selector]') { - $this->addOrderItem($items, $form, $form_state); + $order = $this->addOrderItem($items, $form, $form_state); } elseif (strpos($trigger_element['#name'], 'adjust_order_') === 0) { - $this->adjustOrderItem($items, $form, $form_state); + $order = $this->adjustOrderItem($items, $form, $form_state); + } + elseif (preg_match('/^order_items\[target_id\]\[order_items\]\[([0-9])*\]\[unit_price\]\[number\]$/', $trigger_element['#name'])) { + $order = $this->updateUnitPrice($items, $form, $form_state); + } + elseif (preg_match('/^order_items\[target_id\]\[order_items\]\[([0-9])*\]\[quantity\]$/', $trigger_element['#name'])) { + $order = $this->updateQuantityFromForm($items, $form, $form_state); + } + if ($order) { + // Update the order on the form. + $form_object = $form_state->getFormObject(); + $form_object->setEntity($order); + + // Remove the user input as we no longer need it. + $user_input = $form_state->getUserInput(); + unset($user_input['order_items']); + $form_state->setUserInput($user_input); } } /** - * Adjusts an order item quantity. + * Adjusts an order item quantity when button pressed. * * @param \Drupal\Core\Field\FieldItemListInterface $items * Values for this field. @@ -289,15 +357,54 @@ * The form array. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state. + * + * @return \Drupal\commerce_order\Entity\Order + * The updated commerce order. */ protected function adjustOrderItem(FieldItemListInterface $items, array &$form, FormStateInterface &$form_state) { $trigger_element = $form_state->getTriggeringElement(); $order_item = OrderItem::load($trigger_element['#order_item_id']); $new_quantity = $order_item->getQuantity() + $trigger_element['#quantity']; + return $this->updateQuantity($order_item, $new_quantity); + } + + /** + * Updates an order item quantity from form field. + * + * @param \Drupal\Core\Field\FieldItemListInterface $items + * Values for this field. + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return \Drupal\commerce_order\Entity\Order + * The updated commerce order. + */ + protected function updateQuantityFromForm(FieldItemListInterface $items, array &$form, FormStateInterface &$form_state) { + $trigger_element = $form_state->getTriggeringElement(); + $order_item = OrderItem::load($trigger_element['#order_item_id']); + $value_key = $trigger_element['#parents']; + $new_quantity = $form_state->getValue($value_key); + return $this->updateQuantity($order_item, $new_quantity); + } + + /** + * Updates an order item quantity. + * + * @param \Drupal\commerce_order\Entity\OrderItem $order_item + * The order item. + * @param int $new_quantity + * The new quantity. + * + * @return \Drupal\commerce_order\Entity\Order + * The updated commerce order. + */ + protected function updateQuantity(OrderItem $order_item, $new_quantity) { /** @var \Drupal\commerce_order\Entity\Order $order */ $order = $order_item->getOrder(); if ($new_quantity > 0) { - $order_item->setQuantity($order_item->getQuantity() + $trigger_element['#quantity']); + $order_item->setQuantity($new_quantity); $order_item->save(); } else { @@ -306,8 +413,36 @@ } $order->recalculateTotalPrice()->save(); - $form_object = $form_state->getFormObject(); - $form_object->setEntity($order); + return $order; + } + + /** + * Updates the unit price for an order item. + * + * @param \Drupal\Core\Field\FieldItemListInterface $items + * Values for this field. + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return \Drupal\commerce_order\Entity\Order + * The updated commerce order. + */ + protected function updateUnitPrice(FieldItemListInterface $items, array &$form, FormStateInterface &$form_state) { + $trigger_element = $form_state->getTriggeringElement(); + $value_key = $trigger_element['#parents']; + array_pop($value_key); + $value = $form_state->getValue($value_key); + // Get the order id from the parent. + $order_item = OrderItem::load($items->get($value_key[3])->getValue()['target_id']); + /** @var \Drupal\commerce_order\Entity\Order $order */ + $order = $order_item->getOrder(); + + $order_item->setUnitPrice(new Price($value['number'], $value['currency_code']), TRUE)->save(); + $order->recalculateTotalPrice()->save(); + + return $order; } /** @@ -319,6 +454,9 @@ * The form array. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state. + * + * @return \Drupal\commerce_order\Entity\Order|false + * The updated commerce order if it has been updated, or FALSE if not. */ protected function addOrderItem(FieldItemListInterface $items, array &$form, FormStateInterface &$form_state) { // Loading the product variation object. @@ -326,14 +464,9 @@ // If we've not loaded a product variation then exit doing nothing. if (!$product_variation) { // There's nothing to do. - return; + return FALSE; } - // Remove the user input as we no longer need it. - $user_input = $form_state->getUserInput(); - unset($user_input['order_items']['target_id']['product_selector']); - $form_state->setUserInput($user_input); - // If there is already an order item for this variation add to the quantity. $create_new_order_item = TRUE; // @todo For some reason the following doesn't work. @@ -367,8 +500,7 @@ $items->appendItem($order_item); } - $form_object = $form_state->getFormObject(); - $form_object->setEntity($order); + return $order; } /** diff -u b/tests/src/FunctionalJavascript/PosFormTest.php b/tests/src/FunctionalJavascript/PosFormTest.php --- b/tests/src/FunctionalJavascript/PosFormTest.php +++ b/tests/src/FunctionalJavascript/PosFormTest.php @@ -87,9 +87,10 @@ // Assert that the product is listed as expected. $web_assert->pageTextContains('Jumper'); - $web_assert->pageTextContains('Quantity 1.00'); - $web_assert->pageTextContains('Price $50.00'); - $web_assert->pageTextContains('Total price $50.00'); + $web_assert->fieldValueEquals('order_items[target_id][order_items][0][quantity]', '1.00'); + // @todo Once Once https://www.drupal.org/project/commerce/issues/2923388 is + // fixed, uncomment. + // $web_assert->fieldValueEquals('order_items[target_id][order_items][0][unit_price][number]', '50.00'); $web_assert->pageTextContains('Remaining Balance $50.00'); // After selecting something from the autocomplete list the value should be @@ -107,9 +108,10 @@ $web_assert->assertWaitOnAjaxRequest(); $web_assert->pageTextContains('Jumper'); - $web_assert->pageTextContains('Quantity 2.00'); - $web_assert->pageTextContains('Price $50.00'); - $web_assert->pageTextContains('Total price $100.00'); + $web_assert->fieldValueEquals('order_items[target_id][order_items][0][quantity]', '2.00'); + // @todo Once Once https://www.drupal.org/project/commerce/issues/2923388 is + // fixed, uncomment. + // $web_assert->fieldValueEquals('order_items[target_id][order_items][0][unit_price][number]', '50.00'); $web_assert->pageTextContains('Remaining Balance $100.00'); // Add a T-Shirt. @@ -123,20 +125,31 @@ $web_assert->assertWaitOnAjaxRequest(); $web_assert->pageTextContains('T-Shirt'); - $web_assert->pageTextContains('Quantity 1.00'); - $web_assert->pageTextContains('Price $23.20'); - $web_assert->pageTextContains('Total price $23.20'); + $web_assert->fieldValueEquals('order_items[target_id][order_items][1][quantity]', '1.00'); + // @todo Once Once https://www.drupal.org/project/commerce/issues/2923388 is + // fixed, uncomment. + // $web_assert->fieldValueEquals('order_items[target_id][order_items][1][unit_price][number]', '23.20'); $web_assert->pageTextContains('Remaining Balance $123.20'); // Click on the buttons to add another Jumper. $this->getSession()->getPage()->findButton('adjust_order_add_4')->click(); $web_assert->assertWaitOnAjaxRequest(); $web_assert->pageTextContains('Jumper'); - $web_assert->pageTextContains('Quantity 3.00'); - $web_assert->pageTextContains('Price $50.00'); - $web_assert->pageTextContains('Total price $150.00'); + $web_assert->fieldValueEquals('order_items[target_id][order_items][0][quantity]', '3.00'); + // @todo Once Once https://www.drupal.org/project/commerce/issues/2923388 is + // fixed, uncomment. + // $web_assert->fieldValueEquals('order_items[target_id][order_items][0][unit_price][number]', '50.00'); $web_assert->pageTextContains('Remaining Balance $173.20'); + // Change the price of jumpers on the form. + // @todo Once https://www.drupal.org/project/commerce/issues/2923388 is + // fixed, add test back. + // $this->getSession()->getPage()->fillField('order_items[target_id][order_items][0][unit_price][number]', '40.50'); + // $web_assert->assertWaitOnAjaxRequest(); + // $web_assert->fieldValueEquals('order_items[target_id][order_items][0][unit_price][number]', '40.50'); + // // (3 * 40.5) + (1 * 23.20) + // $web_assert->pageTextContains('Remaining Balance $144.70'); + // Click on the buttons to remove all the jumpers. $button = $this->getSession()->getPage()->findButton('adjust_order_remove_4'); for ($i = 0; $i <= 2; $i++) { @@ -144,12 +157,24 @@ $web_assert->assertWaitOnAjaxRequest(); } $web_assert->pageTextNotContains('Jumper'); - $web_assert->pageTextNotContains('Price $50.00'); $web_assert->pageTextContains('T-Shirt'); + // @todo Once Once https://www.drupal.org/project/commerce/issues/2923388 is + // fixed, uncomment. + // $web_assert->fieldValueEquals('order_items[target_id][order_items][0][unit_price][number]', '23.20'); $web_assert->pageTextContains('Remaining Balance $23.20'); - // Click on the buttons to remove the T-Shirt. - $this->getSession()->getPage()->findButton('adjust_order_remove_1')->click(); + // Set the quantity of t-shirts using the quantity form field. + $this->getSession()->getPage()->fillField('order_items[target_id][order_items][0][quantity]', '10'); + $web_assert->assertWaitOnAjaxRequest(); + $web_assert->pageTextContains('T-Shirt'); + // @todo Once Once https://www.drupal.org/project/commerce/issues/2923388 is + // fixed, uncomment. + // $web_assert->fieldValueEquals('order_items[target_id][order_items][0][unit_price][number]', '23.20'); + $web_assert->fieldValueEquals('order_items[target_id][order_items][0][quantity]', '10.00'); + $web_assert->pageTextContains('Remaining Balance $232.00'); + + // Set the quantity to 0 to remove the T-Shirt. + $this->getSession()->getPage()->fillField('order_items[target_id][order_items][0][quantity]', '0'); $web_assert->assertWaitOnAjaxRequest(); $web_assert->pageTextNotContains('T-Shirt'); $web_assert->pageTextContains('Remaining Balance $0.00');