diff --git a/app/code/Magento/Sales/Api/Data/CommentInterface.php b/app/code/Magento/Sales/Api/Data/CommentInterface.php index d7021dc9f9546..fcab786319340 100644 --- a/app/code/Magento/Sales/Api/Data/CommentInterface.php +++ b/app/code/Magento/Sales/Api/Data/CommentInterface.php @@ -12,6 +12,16 @@ */ interface CommentInterface { + /* + * Is-visible-on-storefront flag. + */ + const IS_VISIBLE_ON_FRONT = 'is_visible_on_front'; + + /* + * Comment. + */ + const COMMENT = 'comment'; + /** * Gets the comment for the invoice. * diff --git a/app/code/Magento/Sales/Api/Data/EntityInterface.php b/app/code/Magento/Sales/Api/Data/EntityInterface.php new file mode 100644 index 0000000000000..d09b25920f899 --- /dev/null +++ b/app/code/Magento/Sales/Api/Data/EntityInterface.php @@ -0,0 +1,53 @@ +orderRepository = $orderRepository; $this->invoiceDocumentFactory = $invoiceDocumentFactory; $this->invoiceValidator = $invoiceValidator; + $this->orderValidator = $orderValidator; $this->paymentAdapter = $paymentAdapter; $this->orderStateResolver = $orderStateResolver; $this->config = $config; @@ -147,7 +158,16 @@ public function execute( ($appendComment && $notify), $arguments ); - $errorMessages = $this->invoiceValidator->validate($invoice, $order); + $errorMessages = array_merge( + $this->invoiceValidator->validate( + $invoice, + [InvoiceQuantityValidator::class] + ), + $this->orderValidator->validate( + $order, + [CanInvoice::class] + ) + ); if (!empty($errorMessages)) { throw new \Magento\Sales\Exception\DocumentValidationException( __("Invoice Document Validation Error(s):\n" . implode("\n", $errorMessages)) diff --git a/app/code/Magento/Sales/Model/Order/Invoice/CommentCreation.php b/app/code/Magento/Sales/Model/Order/Invoice/CommentCreation.php new file mode 100644 index 0000000000000..fa53f72ebcafc --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Invoice/CommentCreation.php @@ -0,0 +1,98 @@ +comment; + } + + /** + * Sets the comment for the invoice. + * + * @param string $comment + * @return $this + */ + public function setComment($comment) + { + $this->comment = $comment; + return $this; + } + + /** + * Gets the is-visible-on-storefront flag value for the invoice. + * + * @return int Is-visible-on-storefront flag value. + */ + public function getIsVisibleOnFront() + { + return $this->isVisibleOnFront; + } + + /** + * Sets the is-visible-on-storefront flag value for the invoice. + * + * @param int $isVisibleOnFront + * @return $this + */ + public function setIsVisibleOnFront($isVisibleOnFront) + { + $this->isVisibleOnFront = $isVisibleOnFront; + return $this; + } + + /** + * Retrieve existing extension attributes object or create a new one. + * + * @return \Magento\Sales\Api\Data\InvoiceCommentCreationExtensionInterface|null + */ + public function getExtensionAttributes() + { + return $this->extensionAttributes; + } + + /** + * Set an extension attributes object. + * + * @param \Magento\Sales\Api\Data\InvoiceCommentCreationExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\InvoiceCommentCreationExtensionInterface $extensionAttributes + ) { + $this->extensionAttributes = $extensionAttributes; + return $this; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Invoice/InvoiceValidator.php b/app/code/Magento/Sales/Model/Order/Invoice/InvoiceValidator.php new file mode 100644 index 0000000000000..cbb68edaa8a55 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Invoice/InvoiceValidator.php @@ -0,0 +1,36 @@ +validator = $validator; + } + + /** + * @inheritdoc + */ + public function validate(InvoiceInterface $entity, array $validators) + { + return $this->validator->validate($entity, $validators); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Invoice/InvoiceValidatorInterface.php b/app/code/Magento/Sales/Model/Order/Invoice/InvoiceValidatorInterface.php new file mode 100644 index 0000000000000..568019a40fce5 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Invoice/InvoiceValidatorInterface.php @@ -0,0 +1,24 @@ +qty = $qty; } + + /** + * Retrieve existing extension attributes object or create a new one. + * + * @return \Magento\Sales\Api\Data\InvoiceItemCreationExtensionInterface|null + */ + public function getExtensionAttributes() + { + return $this->extensionAttributes; + } + + /** + * Set an extension attributes object. + * + * @param \Magento\Sales\Api\Data\InvoiceItemCreationExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\InvoiceItemCreationExtensionInterface $extensionAttributes + ) { + $this->extensionAttributes = $extensionAttributes; + return $this; + } } diff --git a/app/code/Magento/Sales/Model/Order/InvoiceValidator.php b/app/code/Magento/Sales/Model/Order/InvoiceQuantityValidator.php similarity index 73% rename from app/code/Magento/Sales/Model/Order/InvoiceValidator.php rename to app/code/Magento/Sales/Model/Order/InvoiceQuantityValidator.php index 35222599fc69e..9ae81dacb0a17 100644 --- a/app/code/Magento/Sales/Model/Order/InvoiceValidator.php +++ b/app/code/Magento/Sales/Model/Order/InvoiceQuantityValidator.php @@ -9,42 +9,38 @@ use Magento\Sales\Api\Data\InvoiceInterface; use Magento\Sales\Api\Data\InvoiceItemInterface; use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ValidatorInterface; /** * Interface InvoiceValidatorInterface */ -class InvoiceValidator implements InvoiceValidatorInterface +class InvoiceQuantityValidator implements ValidatorInterface { /** - * @var OrderValidatorInterface + * @var OrderRepositoryInterface */ - private $orderValidator; + private $orderRepository; /** * InvoiceValidator constructor. - * @param OrderValidatorInterface $orderValidator + * @param OrderRepositoryInterface $orderRepository */ - public function __construct(OrderValidatorInterface $orderValidator) + public function __construct(OrderRepositoryInterface $orderRepository) { - $this->orderValidator = $orderValidator; + $this->orderRepository = $orderRepository; } /** - * @param InvoiceInterface $invoice - * @param OrderInterface $order - * @return array + * @inheritdoc */ - public function validate(InvoiceInterface $invoice, OrderInterface $order) + public function validate($invoice) { - $messages = $this->checkQtyAvailability($invoice, $order); - - if (!$this->orderValidator->canInvoice($order)) { - $messages[] = __( - 'An invoice cannot be created when an order has a status of %1.', - $order->getStatus() - ); + if ($invoice->getOrderId() === null) { + return [__('Order Id is required for invoice document')]; } - return $messages; + $order = $this->orderRepository->get($invoice->getOrderId()); + return $this->checkQtyAvailability($invoice, $order); } /** diff --git a/app/code/Magento/Sales/Model/Order/InvoiceValidatorInterface.php b/app/code/Magento/Sales/Model/Order/InvoiceValidatorInterface.php deleted file mode 100644 index 64b2f98dfe37e..0000000000000 --- a/app/code/Magento/Sales/Model/Order/InvoiceValidatorInterface.php +++ /dev/null @@ -1,25 +0,0 @@ -validator = $validator; + } + + /** + * @inheritdoc */ - public function canInvoice(OrderInterface $order) + public function validate(OrderInterface $entity, array $validators) { - if ($order->getState() === Order::STATE_PAYMENT_REVIEW || - $order->getState() === Order::STATE_HOLDED || - $order->getState() === Order::STATE_CANCELED || - $order->getState() === Order::STATE_COMPLETE || - $order->getState() === Order::STATE_CLOSED - ) { - return false; - }; - /** @var \Magento\Sales\Model\Order\Item $item */ - foreach ($order->getItems() as $item) { - if ($item->getQtyToInvoice() > 0 && !$item->getLockedDoInvoice()) { - return true; - } - } - return false; + return $this->validator->validate($entity, $validators); } } diff --git a/app/code/Magento/Sales/Model/Order/OrderValidatorInterface.php b/app/code/Magento/Sales/Model/Order/OrderValidatorInterface.php index d0dcc38af642a..c5a9a6c1d3296 100644 --- a/app/code/Magento/Sales/Model/Order/OrderValidatorInterface.php +++ b/app/code/Magento/Sales/Model/Order/OrderValidatorInterface.php @@ -3,21 +3,22 @@ * Copyright © 2016 Magento. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Sales\Model\Order; use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Exception\DocumentValidationException; +use Magento\Sales\Model\ValidatorInterface; /** * Interface OrderValidatorInterface - * - * @api */ interface OrderValidatorInterface { /** - * @param OrderInterface $order - * @return bool + * @param OrderInterface $entity + * @param ValidatorInterface[] $validators + * @return string[] + * @throws DocumentValidationException */ - public function canInvoice(OrderInterface $order); + public function validate(OrderInterface $entity, array $validators); } diff --git a/app/code/Magento/Sales/Model/Order/Shipment.php b/app/code/Magento/Sales/Model/Order/Shipment.php index 6647bae750fff..2277f92d6e0e8 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment.php +++ b/app/code/Magento/Sales/Model/Order/Shipment.php @@ -405,7 +405,7 @@ public function addTrack(\Magento\Sales\Model\Order\Shipment\Track $track) * Adds comment to shipment with additional possibility to send it to customer via email * and show it in customer account * - * @param \Magento\Sales\Model\Order\Shipment\Comment $comment + * @param \Magento\Sales\Model\Order\Shipment\Comment|string $comment * @param bool $notify * @param bool $visibleOnFront * @return $this diff --git a/app/code/Magento/Sales/Model/Order/Shipment/CommentCreation.php b/app/code/Magento/Sales/Model/Order/Shipment/CommentCreation.php new file mode 100644 index 0000000000000..19d06fb0eff32 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Shipment/CommentCreation.php @@ -0,0 +1,96 @@ +extensionAttributes; + } + + /** + * Set an extension attributes object. + * + * @param \Magento\Sales\Api\Data\ShipmentCommentCreationExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\ShipmentCommentCreationExtensionInterface $extensionAttributes + ) { + $this->extensionAttributes = $extensionAttributes; + return $this; + } + + /** + * Gets the comment for the invoice. + * + * @return string Comment. + */ + public function getComment() + { + return $this->comment; + } + + /** + * Sets the comment for the invoice. + * + * @param string $comment + * @return $this + */ + public function setComment($comment) + { + $this->comment = $comment; + return $this; + } + + /** + * Gets the is-visible-on-storefront flag value for the invoice. + * + * @return int Is-visible-on-storefront flag value. + */ + public function getIsVisibleOnFront() + { + return $this->isVisibleOnFront; + } + + /** + * Sets the is-visible-on-storefront flag value for the invoice. + * + * @param int $isVisibleOnFront + * @return $this + */ + public function setIsVisibleOnFront($isVisibleOnFront) + { + $this->isVisibleOnFront = $isVisibleOnFront; + return $this; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Shipment/CreationArguments.php b/app/code/Magento/Sales/Model/Order/Shipment/CreationArguments.php new file mode 100644 index 0000000000000..8a43a73553e79 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Shipment/CreationArguments.php @@ -0,0 +1,37 @@ +extensionAttributes; + } + + /** + * {@inheritdoc} + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\ShipmentCreationArgumentsExtensionInterface $extensionAttributes + ) { + $this->extensionAttributes = $extensionAttributes; + return $this; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Shipment/Item.php b/app/code/Magento/Sales/Model/Order/Shipment/Item.php index c7fdca853b17e..8627f76031b06 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment/Item.php +++ b/app/code/Magento/Sales/Model/Order/Shipment/Item.php @@ -151,22 +151,7 @@ public function getOrderItem() */ public function setQty($qty) { - if ($this->getOrderItem()->getIsQtyDecimal()) { - $qty = (double)$qty; - } else { - $qty = (int)$qty; - } - $qty = $qty > 0 ? $qty : 0; - /** - * Check qty availability - */ - if ($qty <= $this->getOrderItem()->getQtyToShip() || $this->getOrderItem()->isDummy(true)) { - $this->setData('qty', $qty); - } else { - throw new \Magento\Framework\Exception\LocalizedException( - __('We found an invalid quantity to ship for item "%1".', $this->getName()) - ); - } + $this->setData('qty', $qty); return $this; } @@ -174,6 +159,7 @@ public function setQty($qty) * Applying qty to order item * * @return $this + * @throws \Magento\Framework\Exception\LocalizedException */ public function register() { diff --git a/app/code/Magento/Sales/Model/Order/Shipment/ItemCreation.php b/app/code/Magento/Sales/Model/Order/Shipment/ItemCreation.php new file mode 100644 index 0000000000000..e3cb2f23d7cf3 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Shipment/ItemCreation.php @@ -0,0 +1,87 @@ +orderItemId; + } + + /** + * {@inheritdoc} + */ + public function setOrderItemId($orderItemId) + { + $this->orderItemId = $orderItemId; + } + + /** + * {@inheritdoc} + */ + public function getQty() + { + return $this->qty; + } + + /** + * {@inheritdoc} + */ + public function setQty($qty) + { + $this->qty = $qty; + } + + /** + * {@inheritdoc} + * + * @return \Magento\Sales\Api\Data\ShipmentItemCreationExtensionInterface|null + */ + public function getExtensionAttributes() + { + return $this->extensionAttributes; + } + + /** + * {@inheritdoc} + * + * @param \Magento\Sales\Api\Data\ShipmentItemCreationExtensionInterface $extensionAttributes + * @return $this + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\ShipmentItemCreationExtensionInterface $extensionAttributes + ) { + $this->extensionAttributes = $extensionAttributes; + return $this; + } + //@codeCoverageIgnoreEnd +} diff --git a/app/code/Magento/Sales/Model/Order/Shipment/Notifier.php b/app/code/Magento/Sales/Model/Order/Shipment/Notifier.php new file mode 100644 index 0000000000000..21dd5ad4a58f6 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Shipment/Notifier.php @@ -0,0 +1,41 @@ +senders = $senders; + } + + /** + * {@inheritdoc} + */ + public function notify( + \Magento\Sales\Api\Data\OrderInterface $order, + \Magento\Sales\Api\Data\ShipmentInterface $shipment, + \Magento\Sales\Api\Data\ShipmentCommentCreationInterface $comment = null, + $forceSyncMode = false + ) { + foreach ($this->senders as $sender) { + $sender->send($order, $shipment, $comment, $forceSyncMode); + } + } +} diff --git a/app/code/Magento/Sales/Model/Order/Shipment/NotifierInterface.php b/app/code/Magento/Sales/Model/Order/Shipment/NotifierInterface.php new file mode 100644 index 0000000000000..f34eb6178d094 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Shipment/NotifierInterface.php @@ -0,0 +1,31 @@ +getItems() as $item) { + if ($item->getQty() > 0) { + $item->register(); + } + } + return $order; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Shipment/OrderRegistrarInterface.php b/app/code/Magento/Sales/Model/Order/Shipment/OrderRegistrarInterface.php new file mode 100644 index 0000000000000..7d54acece3599 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Shipment/OrderRegistrarInterface.php @@ -0,0 +1,26 @@ +extensionAttributes; + } + + /** + * {@inheritdoc} + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\ShipmentPackageExtensionInterface $extensionAttributes + ) { + $this->extensionAttributes = $extensionAttributes; + return $this; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Shipment/PackageCreation.php b/app/code/Magento/Sales/Model/Order/Shipment/PackageCreation.php new file mode 100644 index 0000000000000..50ad944b8251c --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Shipment/PackageCreation.php @@ -0,0 +1,36 @@ +extensionAttributes; + } + + /** + * {@inheritdoc} + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\ShipmentPackageCreationExtensionInterface $extensionAttributes + ) { + $this->extensionAttributes = $extensionAttributes; + return $this; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php new file mode 100644 index 0000000000000..228a45ff16aae --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php @@ -0,0 +1,149 @@ +paymentHelper = $paymentHelper; + $this->shipmentResource = $shipmentResource; + $this->globalConfig = $globalConfig; + $this->eventManager = $eventManager; + } + + /** + * Sends order shipment email to the customer. + * + * Email will be sent immediately in two cases: + * + * - if asynchronous email sending is disabled in global settings + * - if $forceSyncMode parameter is set to TRUE + * + * Otherwise, email will be sent later during running of + * corresponding cron job. + * + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param \Magento\Sales\Api\Data\ShipmentInterface $shipment + * @param \Magento\Sales\Api\Data\ShipmentCommentCreationInterface|null $comment + * @param bool $forceSyncMode + * + * @return bool + */ + public function send( + \Magento\Sales\Api\Data\OrderInterface $order, + \Magento\Sales\Api\Data\ShipmentInterface $shipment, + \Magento\Sales\Api\Data\ShipmentCommentCreationInterface $comment = null, + $forceSyncMode = false + ) { + $shipment->setSendEmail(true); + + if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { + $transport = [ + 'order' => $order, + 'shipment' => $shipment, + 'comment' => $comment ? $comment->getComment() : '', + 'billing' => $order->getBillingAddress(), + 'payment_html' => $this->getPaymentHtml($order), + 'store' => $order->getStore(), + 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), + 'formattedBillingAddress' => $this->getFormattedBillingAddress($order) + ]; + + $this->eventManager->dispatch( + 'email_shipment_set_template_vars_before', + ['sender' => $this, 'transport' => $transport] + ); + + $this->templateContainer->setTemplateVars($transport); + + if ($this->checkAndSend($order)) { + $shipment->setEmailSent(true); + + $this->shipmentResource->saveAttribute($shipment, ['send_email', 'email_sent']); + + return true; + } + } else { + $shipment->setEmailSent(null); + + $this->shipmentResource->saveAttribute($shipment, 'email_sent'); + } + + $this->shipmentResource->saveAttribute($shipment, 'send_email'); + + return false; + } + + /** + * Returns payment info block as HTML. + * + * @param \Magento\Sales\Api\Data\OrderInterface $order + * + * @return string + */ + private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) + { + return $this->paymentHelper->getInfoBlockHtml( + $order->getPayment(), + $this->identityContainer->getStore()->getStoreId() + ); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Shipment/SenderInterface.php b/app/code/Magento/Sales/Model/Order/Shipment/SenderInterface.php new file mode 100644 index 0000000000000..a030038b7b139 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Shipment/SenderInterface.php @@ -0,0 +1,29 @@ +validator = $validator; + } + + /** + * @inheritdoc + */ + public function validate(ShipmentInterface $entity, array $validators) + { + return $this->validator->validate($entity, $validators); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Shipment/ShipmentValidatorInterface.php b/app/code/Magento/Sales/Model/Order/Shipment/ShipmentValidatorInterface.php new file mode 100644 index 0000000000000..198a4019bf6b8 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Shipment/ShipmentValidatorInterface.php @@ -0,0 +1,24 @@ +trackNumber; + } + + /** + * {@inheritdoc} + */ + public function setTrackNumber($trackNumber) + { + $this->trackNumber = $trackNumber; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getTitle() + { + return $this->title; + } + + /** + * {@inheritdoc} + */ + public function setTitle($title) + { + $this->title = $title; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getCarrierCode() + { + return $this->carrierCode; + } + + /** + * {@inheritdoc} + */ + public function setCarrierCode($carrierCode) + { + $this->carrierCode = $carrierCode; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getExtensionAttributes() + { + return $this->extensionAttributes; + } + + /** + * {@inheritdoc} + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\ShipmentTrackCreationExtensionInterface $extensionAttributes + ) { + $this->extensionAttributes = $extensionAttributes; + return $this; + } + + //@codeCoverageIgnoreEnd +} diff --git a/app/code/Magento/Sales/Model/Order/Shipment/Validation/QuantityValidator.php b/app/code/Magento/Sales/Model/Order/Shipment/Validation/QuantityValidator.php new file mode 100644 index 0000000000000..20e3712d889ed --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Shipment/Validation/QuantityValidator.php @@ -0,0 +1,108 @@ +orderRepository = $orderRepository; + } + + /** + * @param ShipmentInterface $entity + * @return array + * @throws DocumentValidationException + * @throws NoSuchEntityException + */ + public function validate($entity) + { + if ($entity->getOrderId() === null) { + return [__('Order Id is required for shipment document')]; + } + + if (empty($entity->getItems())) { + return [__('You can\'t create a shipment without products.')]; + } + $messages = []; + + $order = $this->orderRepository->get($entity->getOrderId()); + $orderItemsById = $this->getOrderItems($order); + + $totalQuantity = 0; + foreach ($entity->getItems() as $item) { + if (!isset($orderItemsById[$item->getOrderItemId()])) { + $messages[] = __( + 'The shipment contains product SKU "%1" that is not part of the original order.', + $item->getSku() + ); + continue; + } + $orderItem = $orderItemsById[$item->getOrderItemId()]; + + if (!$this->isQtyAvailable($orderItem, $item->getQty())) { + $messages[] =__( + 'The quantity to ship must not be greater than the unshipped quantity' + . ' for product SKU "%1".', + $orderItem->getSku() + ); + } else { + $totalQuantity += $item->getQty(); + } + } + if ($totalQuantity <= 0) { + $messages[] = __('You can\'t create a shipment without products.'); + } + + return $messages; + } + + /** + * @param OrderInterface $order + * @return OrderItemInterface[] + */ + private function getOrderItems(OrderInterface $order) + { + $orderItemsById = []; + foreach ($order->getItems() as $item) { + $orderItemsById[$item->getItemId()] = $item; + } + + return $orderItemsById; + } + + /** + * @param Item $orderItem + * @param int $qty + * @return bool + */ + private function isQtyAvailable(Item $orderItem, $qty) + { + return $qty <= $orderItem->getQtyToShip() || $orderItem->isDummy(true); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Shipment/Validation/TrackValidator.php b/app/code/Magento/Sales/Model/Order/Shipment/Validation/TrackValidator.php new file mode 100644 index 0000000000000..55970d37c597d --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Shipment/Validation/TrackValidator.php @@ -0,0 +1,33 @@ +getTracks()) { + return $messages; + } + foreach ($entity->getTracks() as $track) { + if (!$track->getTrackNumber()) { + $messages[] = __('Please enter a tracking number.'); + } + } + return $messages; + } +} diff --git a/app/code/Magento/Sales/Model/Order/ShipmentDocumentFactory.php b/app/code/Magento/Sales/Model/Order/ShipmentDocumentFactory.php new file mode 100644 index 0000000000000..d10f84d815543 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/ShipmentDocumentFactory.php @@ -0,0 +1,128 @@ +shipmentFactory = $shipmentFactory; + $this->trackFactory = $trackFactory; + $this->hydratorPool = $hydratorPool; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @param OrderInterface $order + * @param ShipmentItemCreationInterface[] $items + * @param ShipmentTrackCreationInterface[] $tracks + * @param ShipmentCommentCreationInterface|null $comment + * @param bool $appendComment + * @param ShipmentPackageCreationInterface[] $packages + * @param ShipmentCreationArgumentsInterface|null $arguments + * @return ShipmentInterface + */ + public function create( + OrderInterface $order, + array $items = [], + array $tracks = [], + ShipmentCommentCreationInterface $comment = null, + $appendComment = false, + array $packages = [], + ShipmentCreationArgumentsInterface $arguments = null + ) { + $shipmentItems = $this->itemsToArray($items); + /** @var Shipment $shipment */ + $shipment = $this->shipmentFactory->create( + $order, + $shipmentItems + ); + $this->prepareTracks($shipment, $tracks); + if ($comment) { + $shipment->addComment( + $comment->getComment(), + $appendComment, + $comment->getIsVisibleOnFront() + ); + } + + return $shipment; + } + + /** + * Adds tracks to the shipment. + * + * @param ShipmentInterface $shipment + * @param ShipmentTrackCreationInterface[] $tracks + * @return ShipmentInterface + */ + private function prepareTracks(\Magento\Sales\Api\Data\ShipmentInterface $shipment, array $tracks) + { + foreach ($tracks as $track) { + $hydrator = $this->hydratorPool->getHydrator( + \Magento\Sales\Api\Data\ShipmentTrackCreationInterface::class + ); + $shipment->addTrack($this->trackFactory->create(['data' => $hydrator->extract($track)])); + } + return $shipment; + } + + /** + * Convert items to array + * + * @param ShipmentItemCreationInterface[] $items + * @return array + */ + private function itemsToArray(array $items = []) + { + $shipmentItems = []; + foreach ($items as $item) { + $shipmentItems[$item->getOrderItemId()] = $item->getQty(); + } + return $shipmentItems; + } +} diff --git a/app/code/Magento/Sales/Model/Order/ShipmentFactory.php b/app/code/Magento/Sales/Model/Order/ShipmentFactory.php index 2ac012760ee47..a8839c7537587 100644 --- a/app/code/Magento/Sales/Model/Order/ShipmentFactory.php +++ b/app/code/Magento/Sales/Model/Order/ShipmentFactory.php @@ -5,6 +5,10 @@ */ namespace Magento\Sales\Model\Order; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; +use Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface; + /** * Factory class for @see \Magento\Sales\Api\Data\ShipmentInterface */ @@ -72,6 +76,8 @@ public function create(\Magento\Sales\Model\Order $order, array $items = [], $tr * @param \Magento\Sales\Model\Order $order * @param array $items * @return \Magento\Sales\Api\Data\ShipmentInterface + * @throws LocalizedException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function prepareItems( \Magento\Sales\Api\Data\ShipmentInterface $shipment, @@ -79,7 +85,6 @@ protected function prepareItems( array $items = [] ) { $totalQty = 0; - foreach ($order->getAllItems() as $orderItem) { if (!$this->canShipItem($orderItem, $items)) { continue; @@ -103,7 +108,7 @@ protected function prepareItems( $qty = $bundleSelectionAttributes['qty'] * $items[$orderItem->getParentItemId()]; $qty = min($qty, $orderItem->getSimpleQtyToShip()); - $item->setQty($qty); + $item->setQty($this->castQty($orderItem, $qty)); $shipment->addItem($item); continue; @@ -126,10 +131,9 @@ protected function prepareItems( $totalQty += $qty; - $item->setQty($qty); + $item->setQty($this->castQty($orderItem, $qty)); $shipment->addItem($item); } - return $shipment->setTotalQty($totalQty); } @@ -211,4 +215,20 @@ protected function canShipItem($item, array $items = []) return $item->getQtyToShip() > 0; } } + + /** + * @param Item $item + * @param string|int|float $qty + * @return float|int + */ + private function castQty(\Magento\Sales\Model\Order\Item $item, $qty) + { + if ($item->getIsQtyDecimal()) { + $qty = (double)$qty; + } else { + $qty = (int)$qty; + } + + return $qty > 0 ? $qty : 0; + } } diff --git a/app/code/Magento/Sales/Model/Order/Validation/CanInvoice.php b/app/code/Magento/Sales/Model/Order/Validation/CanInvoice.php new file mode 100644 index 0000000000000..bb14dc1bb5180 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Validation/CanInvoice.php @@ -0,0 +1,66 @@ +isStateReadyForInvoice($entity)) { + $messages[] = __('An invoice cannot be created when an order has a status of %1', $entity->getStatus()); + } elseif (!$this->canInvoice($entity)) { + $messages[] = __('The order does not allow an invoice to be created.'); + } + + return $messages; + } + + /** + * @param OrderInterface $order + * @return bool + */ + private function isStateReadyForInvoice(OrderInterface $order) + { + if ($order->getState() === Order::STATE_PAYMENT_REVIEW || + $order->getState() === Order::STATE_HOLDED || + $order->getState() === Order::STATE_CANCELED || + $order->getState() === Order::STATE_COMPLETE || + $order->getState() === Order::STATE_CLOSED + ) { + return false; + }; + + return true; + } + + /** + * @param OrderInterface $order + * @return bool + */ + private function canInvoice(OrderInterface $order) + { + /** @var \Magento\Sales\Model\Order\Item $item */ + foreach ($order->getItems() as $item) { + if ($item->getQtyToInvoice() > 0 && !$item->getLockedDoInvoice()) { + return true; + } + } + return false; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Validation/CanShip.php b/app/code/Magento/Sales/Model/Order/Validation/CanShip.php new file mode 100644 index 0000000000000..46638a62483e6 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Validation/CanShip.php @@ -0,0 +1,65 @@ +isStateReadyForShipment($entity)) { + $messages[] = __('A shipment cannot be created when an order has a status of %1', $entity->getStatus()); + } elseif (!$this->canShip($entity)) { + $messages[] = __('The order does not allow a shipment to be created.'); + } + + return $messages; + } + + /** + * @param OrderInterface $order + * @return bool + */ + private function isStateReadyForShipment(OrderInterface $order) + { + if ($order->getState() === Order::STATE_PAYMENT_REVIEW || + $order->getState() === Order::STATE_HOLDED || + $order->getIsVirtual() || + $order->getState() === Order::STATE_CANCELED + ) { + return false; + } + + return true; + } + + /** + * @param OrderInterface $order + * @return bool + */ + private function canShip(OrderInterface $order) + { + /** @var \Magento\Sales\Model\Order\Item $item */ + foreach ($order->getItems() as $item) { + if ($item->getQtyToShip() > 0 && !$item->getIsVirtual() && !$item->getLockedDoShip()) { + return true; + } + } + + return false; + } +} diff --git a/app/code/Magento/Sales/Model/ShipOrder.php b/app/code/Magento/Sales/Model/ShipOrder.php new file mode 100644 index 0000000000000..d051144cf73ca --- /dev/null +++ b/app/code/Magento/Sales/Model/ShipOrder.php @@ -0,0 +1,206 @@ +resourceConnection = $resourceConnection; + $this->orderRepository = $orderRepository; + $this->shipmentDocumentFactory = $shipmentDocumentFactory; + $this->shipmentValidator = $shipmentValidator; + $this->orderValidator = $orderValidator; + $this->orderStateResolver = $orderStateResolver; + $this->config = $config; + $this->shipmentRepository = $shipmentRepository; + $this->notifierInterface = $notifierInterface; + $this->logger = $logger; + $this->orderRegistrar = $orderRegistrar; + } + + /** + * @param int $orderId + * @param \Magento\Sales\Api\Data\ShipmentItemCreationInterface[] $items + * @param bool $notify + * @param bool $appendComment + * @param \Magento\Sales\Api\Data\ShipmentCommentCreationInterface|null $comment + * @param \Magento\Sales\Api\Data\ShipmentTrackCreationInterface[] $tracks + * @param \Magento\Sales\Api\Data\ShipmentPackageCreationInterface[] $packages + * @param \Magento\Sales\Api\Data\ShipmentCreationArgumentsInterface|null $arguments + * @return int + * @throws \Magento\Sales\Api\Exception\DocumentValidationExceptionInterface + * @throws \Magento\Sales\Api\Exception\CouldNotShipExceptionInterface + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \DomainException + */ + public function execute( + $orderId, + array $items = [], + $notify = false, + $appendComment = false, + \Magento\Sales\Api\Data\ShipmentCommentCreationInterface $comment = null, + array $tracks = [], + array $packages = [], + \Magento\Sales\Api\Data\ShipmentCreationArgumentsInterface $arguments = null + ) { + $connection = $this->resourceConnection->getConnection('sales'); + $order = $this->orderRepository->get($orderId); + $shipment = $this->shipmentDocumentFactory->create( + $order, + $items, + $tracks, + $comment, + ($appendComment && $notify), + $packages, + $arguments + ); + $orderValidationResult = $this->orderValidator->validate( + $order, + [ + CanShip::class + ] + ); + $shipmentValidationResult = $this->shipmentValidator->validate( + $shipment, + [ + QuantityValidator::class, + TrackValidator::class + ] + ); + $validationMessages = array_merge($orderValidationResult, $shipmentValidationResult); + if (!empty($validationMessages)) { + throw new \Magento\Sales\Exception\DocumentValidationException( + __("Shipment Document Validation Error(s):\n" . implode("\n", $validationMessages)) + ); + } + $connection->beginTransaction(); + try { + $this->orderRegistrar->register($order, $shipment); + $order->setState( + $this->orderStateResolver->getStateForOrder($order, [OrderStateResolverInterface::IN_PROGRESS]) + ); + $order->setStatus($this->config->getStateDefaultStatus($order->getState())); + $this->shipmentRepository->save($shipment); + $this->orderRepository->save($order); + $connection->commit(); + } catch (\Exception $e) { + $this->logger->critical($e); + $connection->rollBack(); + throw new \Magento\Sales\Exception\CouldNotShipException( + __('Could not save a shipment, see error log for details') + ); + } + if ($notify) { + if (!$appendComment) { + $comment = null; + } + $this->notifierInterface->notify($order, $shipment, $comment); + } + return $shipment->getEntityId(); + } +} diff --git a/app/code/Magento/Sales/Model/Validator.php b/app/code/Magento/Sales/Model/Validator.php new file mode 100644 index 0000000000000..b8d57ded29702 --- /dev/null +++ b/app/code/Magento/Sales/Model/Validator.php @@ -0,0 +1,56 @@ +objectManager = $objectManager; + } + + /** + * @param object $entity + * @param ValidatorInterface[] $validators + * @return string[] + * @throws ConfigurationMismatchException + */ + public function validate($entity, array $validators) + { + $messages = []; + foreach ($validators as $validatorName) { + $validator = $this->objectManager->get($validatorName); + if (!$validator instanceof ValidatorInterface) { + throw new ConfigurationMismatchException( + __( + sprintf('Validator %s is not instance of general validator interface', $validatorName) + ) + ); + } + $messages = array_merge($messages, $validator->validate($entity)); + } + + return $messages; + } +} diff --git a/app/code/Magento/Sales/Model/ValidatorInterface.php b/app/code/Magento/Sales/Model/ValidatorInterface.php new file mode 100644 index 0000000000000..1882782e314f7 --- /dev/null +++ b/app/code/Magento/Sales/Model/ValidatorInterface.php @@ -0,0 +1,23 @@ +disableOriginalConstructor() ->getMock(); + $this->orderValidatorMock = $this->getMockBuilder(OrderValidatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->paymentAdapterMock = $this->getMockBuilder(PaymentAdapterInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -172,11 +183,12 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->orderInvoice = new OrderInvoice( + $this->invoiceOrder = new InvoiceOrder( $this->resourceConnectionMock, $this->orderRepositoryMock, $this->invoiceDocumentFactoryMock, $this->invoiceValidatorMock, + $this->orderValidatorMock, $this->paymentAdapterMock, $this->orderStateResolverMock, $this->configMock, @@ -212,7 +224,11 @@ public function testOrderInvoice($orderId, $capture, $items, $notify, $appendCom $this->invoiceValidatorMock->expects($this->once()) ->method('validate') - ->with($this->invoiceMock, $this->orderMock) + ->with($this->invoiceMock) + ->willReturn([]); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) ->willReturn([]); $this->paymentAdapterMock->expects($this->once()) @@ -271,7 +287,7 @@ public function testOrderInvoice($orderId, $capture, $items, $notify, $appendCom $this->assertEquals( 2, - $this->orderInvoice->execute( + $this->invoiceOrder->execute( $orderId, $capture, $items, @@ -311,10 +327,14 @@ public function testDocumentValidationException() $this->invoiceValidatorMock->expects($this->once()) ->method('validate') - ->with($this->invoiceMock, $this->orderMock) + ->with($this->invoiceMock) ->willReturn($errorMessages); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) + ->willReturn([]); - $this->orderInvoice->execute( + $this->invoiceOrder->execute( $orderId, $capture, $items, @@ -356,7 +376,11 @@ public function testCouldNotInvoiceException() $this->invoiceValidatorMock->expects($this->once()) ->method('validate') - ->with($this->invoiceMock, $this->orderMock) + ->with($this->invoiceMock) + ->willReturn([]); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) ->willReturn([]); $e = new \Exception(); @@ -372,7 +396,7 @@ public function testCouldNotInvoiceException() $this->adapterInterface->expects($this->once()) ->method('rollBack'); - $this->orderInvoice->execute( + $this->invoiceOrder->execute( $orderId, $capture, $items, diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceValidatorTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceQuantityValidatorTest.php similarity index 66% rename from app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceValidatorTest.php rename to app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceQuantityValidatorTest.php index 6fdfdb61b3635..8d800e12a6ff0 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceValidatorTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceQuantityValidatorTest.php @@ -6,15 +6,16 @@ namespace Magento\Sales\Test\Unit\Model\Order; +use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Order; /** * Test for \Magento\Sales\Model\Order\InvoiceValidator class */ -class InvoiceValidatorTest extends \PHPUnit_Framework_TestCase +class InvoiceQuantityValidatorTest extends \PHPUnit_Framework_TestCase { /** - * @var \Magento\Sales\Model\Order\InvoiceValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Sales\Model\Order\InvoiceQuantityValidator|\PHPUnit_Framework_MockObject_MockObject */ private $model; @@ -24,14 +25,14 @@ class InvoiceValidatorTest extends \PHPUnit_Framework_TestCase private $objectManager; /** - * @var \Magento\Sales\Model\Order\OrderValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Sales\Api\Data\OrderInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $orderValidatorMock; + private $orderMock; /** - * @var \Magento\Sales\Api\Data\OrderInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Sales\Api\OrderRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $orderMock; + private $orderRepositoryMock; /** * @var \Magento\Sales\Api\Data\InvoiceInterface|\PHPUnit_Framework_MockObject_MockObject @@ -42,24 +43,21 @@ protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->orderValidatorMock = $this->getMockBuilder(\Magento\Sales\Model\Order\OrderValidatorInterface::class) - ->disableOriginalConstructor() - ->setMethods(['canInvoice']) - ->getMockForAbstractClass(); - $this->orderMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderInterface::class) ->disableOriginalConstructor() - ->setMethods(['getStatus']) ->getMockForAbstractClass(); $this->invoiceMock = $this->getMockBuilder(\Magento\Sales\Api\Data\InvoiceInterface::class) ->disableOriginalConstructor() ->setMethods(['getTotalQty', 'getItems']) ->getMockForAbstractClass(); - + $this->orderRepositoryMock = $this->getMockBuilder( + OrderRepositoryInterface::class + )->disableOriginalConstructor()->getMockForAbstractClass(); + $this->orderRepositoryMock->expects($this->any())->method('get')->willReturn($this->orderMock); $this->model = $this->objectManager->getObject( - \Magento\Sales\Model\Order\InvoiceValidator::class, - ['orderValidator' => $this->orderValidatorMock] + \Magento\Sales\Model\Order\InvoiceQuantityValidator::class, + ['orderRepository' => $this->orderRepositoryMock] ); } @@ -75,39 +73,12 @@ public function testValidate() $this->orderMock->expects($this->once()) ->method('getItems') ->willReturn([$orderItemMock]); - $this->orderValidatorMock->expects($this->once()) - ->method('canInvoice') - ->with($this->orderMock) - ->willReturn(true); - $this->assertEquals( - $expectedResult, - $this->model->validate($this->invoiceMock, $this->orderMock) - ); - } - - public function testValidateCanNotInvoiceOrder() - { - $orderStatus = 'Test Status'; - $expectedResult = [__('An invoice cannot be created when an order has a status of %1.', $orderStatus)]; - $invoiceItemMock = $this->getInvoiceItemMock(1, 1); - $this->invoiceMock->expects($this->once()) - ->method('getItems') - ->willReturn([$invoiceItemMock]); - - $orderItemMock = $this->getOrderItemMock(1, 1, true); - $this->orderMock->expects($this->once()) - ->method('getItems') - ->willReturn([$orderItemMock]); - $this->orderMock->expects($this->once()) - ->method('getStatus') - ->willReturn($orderStatus); - $this->orderValidatorMock->expects($this->once()) - ->method('canInvoice') - ->with($this->orderMock) - ->willReturn(false); + $this->invoiceMock->expects($this->exactly(2)) + ->method('getOrderId') + ->willReturn(1); $this->assertEquals( $expectedResult, - $this->model->validate($this->invoiceMock, $this->orderMock) + $this->model->validate($this->invoiceMock) ); } @@ -125,13 +96,12 @@ public function testValidateInvoiceQtyBiggerThanOrder() $this->orderMock->expects($this->once()) ->method('getItems') ->willReturn([$orderItemMock]); - $this->orderValidatorMock->expects($this->once()) - ->method('canInvoice') - ->with($this->orderMock) - ->willReturn(true); + $this->invoiceMock->expects($this->exactly(2)) + ->method('getOrderId') + ->willReturn(1); $this->assertEquals( $expectedResult, - $this->model->validate($this->invoiceMock, $this->orderMock) + $this->model->validate($this->invoiceMock) ); } @@ -146,13 +116,21 @@ public function testValidateNoOrderItems() $this->orderMock->expects($this->once()) ->method('getItems') ->willReturn([]); - $this->orderValidatorMock->expects($this->once()) - ->method('canInvoice') - ->with($this->orderMock) - ->willReturn(true); + $this->invoiceMock->expects($this->exactly(2)) + ->method('getOrderId') + ->willReturn(1); + $this->assertEquals( + $expectedResult, + $this->model->validate($this->invoiceMock) + ); + } + + public function testValidateNoOrder() + { + $expectedResult = [__('Order Id is required for invoice document')]; $this->assertEquals( $expectedResult, - $this->model->validate($this->invoiceMock, $this->orderMock) + $this->model->validate($this->invoiceMock) ); } @@ -169,13 +147,12 @@ public function testValidateNoInvoiceItems() $this->orderMock->expects($this->once()) ->method('getItems') ->willReturn([$orderItemMock]); - $this->orderValidatorMock->expects($this->once()) - ->method('canInvoice') - ->with($this->orderMock) - ->willReturn(true); + $this->invoiceMock->expects($this->exactly(2)) + ->method('getOrderId') + ->willReturn(1); $this->assertEquals( $expectedResult, - $this->model->validate($this->invoiceMock, $this->orderMock) + $this->model->validate($this->invoiceMock) ); } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/OrderRegistrarTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/OrderRegistrarTest.php new file mode 100644 index 0000000000000..e5bff791edcca --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/OrderRegistrarTest.php @@ -0,0 +1,73 @@ +orderMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->shipmentMock = $this->getMockBuilder(\Magento\Sales\Api\Data\ShipmentInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->model = new \Magento\Sales\Model\Order\Shipment\OrderRegistrar(); + } + + public function testRegister() + { + $item1 = $this->getShipmentItemMock(); + $item1->expects($this->once()) + ->method('getQty') + ->willReturn(0); + $item1->expects($this->never()) + ->method('register'); + + $item2 = $this->getShipmentItemMock(); + $item2->expects($this->once()) + ->method('getQty') + ->willReturn(0.5); + $item2->expects($this->once()) + ->method('register'); + + $items = [$item1, $item2]; + $this->shipmentMock->expects($this->once()) + ->method('getItems') + ->willReturn($items); + $this->assertEquals( + $this->orderMock, + $this->model->register($this->orderMock, $this->shipmentMock) + ); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function getShipmentItemMock() + { + return $this->getMockBuilder(\Magento\Sales\Api\Data\ShipmentItemInterface::class) + ->disableOriginalConstructor() + ->setMethods(['register']) + ->getMockForAbstractClass(); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php new file mode 100644 index 0000000000000..8373c7e57d0fe --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php @@ -0,0 +1,361 @@ +orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->setMethods(['getStoreId']) + ->disableOriginalConstructor() + ->getMock(); + + $this->storeMock->expects($this->any()) + ->method('getStoreId') + ->willReturn(1); + $this->orderMock->expects($this->any()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->senderMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Email\Sender::class) + ->disableOriginalConstructor() + ->setMethods(['send', 'sendCopyTo']) + ->getMock(); + + $this->loggerMock = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->shipmentMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment::class) + ->disableOriginalConstructor() + ->setMethods(['setSendEmail', 'setEmailSent']) + ->getMock(); + + $this->commentMock = $this->getMockBuilder(\Magento\Sales\Api\Data\ShipmentCommentCreationInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->commentMock->expects($this->any()) + ->method('getComment') + ->willReturn('Comment text'); + + $this->addressMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Address::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderMock->expects($this->any()) + ->method('getBillingAddress') + ->willReturn($this->addressMock); + $this->orderMock->expects($this->any()) + ->method('getShippingAddress') + ->willReturn($this->addressMock); + + $this->globalConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->paymentInfoMock = $this->getMockBuilder(\Magento\Payment\Model\Info::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderMock->expects($this->any()) + ->method('getPayment') + ->willReturn($this->paymentInfoMock); + + $this->paymentHelperMock = $this->getMockBuilder(\Magento\Payment\Helper\Data::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->paymentHelperMock->expects($this->any()) + ->method('getInfoBlockHtml') + ->with($this->paymentInfoMock, 1) + ->willReturn('Payment Info Block'); + + $this->shipmentResourceMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Shipment::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->addressRendererMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Address\Renderer::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->addressRendererMock->expects($this->any()) + ->method('format') + ->with($this->addressMock, 'html') + ->willReturn('Formatted address'); + + $this->templateContainerMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Email\Container\Template::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->identityContainerMock = $this->getMockBuilder( + \Magento\Sales\Model\Order\Email\Container\ShipmentIdentity::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->identityContainerMock->expects($this->any()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->senderBuilderFactoryMock = $this->getMockBuilder( + \Magento\Sales\Model\Order\Email\SenderBuilderFactory::class + ) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->subject = new \Magento\Sales\Model\Order\Shipment\Sender\EmailSender( + $this->templateContainerMock, + $this->identityContainerMock, + $this->senderBuilderFactoryMock, + $this->loggerMock, + $this->addressRendererMock, + $this->paymentHelperMock, + $this->shipmentResourceMock, + $this->globalConfigMock, + $this->eventManagerMock + ); + } + + /** + * @param int $configValue + * @param bool $forceSyncMode + * @param bool $isComment + * @param bool $emailSendingResult + * + * @dataProvider sendDataProvider + * + * @return void + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testSend($configValue, $forceSyncMode, $isComment, $emailSendingResult) + { + $this->globalConfigMock->expects($this->once()) + ->method('getValue') + ->with('sales_email/general/async_sending') + ->willReturn($configValue); + + if (!$isComment) { + $this->commentMock = null; + } + + $this->shipmentMock->expects($this->once()) + ->method('setSendEmail') + ->with(true); + + if (!$configValue || $forceSyncMode) { + $transport = [ + 'order' => $this->orderMock, + 'shipment' => $this->shipmentMock, + 'comment' => $isComment ? 'Comment text' : '', + 'billing' => $this->addressMock, + 'payment_html' => 'Payment Info Block', + 'store' => $this->storeMock, + 'formattedShippingAddress' => 'Formatted address', + 'formattedBillingAddress' => 'Formatted address', + ]; + + $this->eventManagerMock->expects($this->once()) + ->method('dispatch') + ->with( + 'email_shipment_set_template_vars_before', + [ + 'sender' => $this->subject, + 'transport' => $transport, + ] + ); + + $this->templateContainerMock->expects($this->once()) + ->method('setTemplateVars') + ->with($transport); + + $this->identityContainerMock->expects($this->once()) + ->method('isEnabled') + ->willReturn($emailSendingResult); + + if ($emailSendingResult) { + $this->senderBuilderFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->senderMock); + + $this->senderMock->expects($this->once()) + ->method('send'); + + $this->senderMock->expects($this->once()) + ->method('sendCopyTo'); + + $this->shipmentMock->expects($this->once()) + ->method('setEmailSent') + ->with(true); + + $this->shipmentResourceMock->expects($this->once()) + ->method('saveAttribute') + ->with($this->shipmentMock, ['send_email', 'email_sent']); + + $this->assertTrue( + $this->subject->send( + $this->orderMock, + $this->shipmentMock, + $this->commentMock, + $forceSyncMode + ) + ); + } else { + $this->shipmentResourceMock->expects($this->once()) + ->method('saveAttribute') + ->with($this->shipmentMock, 'send_email'); + + $this->assertFalse( + $this->subject->send( + $this->orderMock, + $this->shipmentMock, + $this->commentMock, + $forceSyncMode + ) + ); + } + } else { + $this->shipmentMock->expects($this->once()) + ->method('setEmailSent') + ->with(null); + + $this->shipmentResourceMock->expects($this->at(0)) + ->method('saveAttribute') + ->with($this->shipmentMock, 'email_sent'); + $this->shipmentResourceMock->expects($this->at(1)) + ->method('saveAttribute') + ->with($this->shipmentMock, 'send_email'); + + $this->assertFalse( + $this->subject->send( + $this->orderMock, + $this->shipmentMock, + $this->commentMock, + $forceSyncMode + ) + ); + } + } + + /** + * @return array + */ + public function sendDataProvider() + { + return [ + 'Successful sync sending with comment' => [0, false, true, true], + 'Successful sync sending without comment' => [0, false, false, true], + 'Failed sync sending with comment' => [0, false, true, false], + 'Successful forced sync sending with comment' => [1, true, true, true], + 'Async sending' => [1, false, false, false], + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Validation/QuantityValidatorTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Validation/QuantityValidatorTest.php new file mode 100644 index 0000000000000..01cccd2458695 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Validation/QuantityValidatorTest.php @@ -0,0 +1,67 @@ +shipmentMock = $this->getMockBuilder(ShipmentInterface::class) + ->getMock(); + $this->shipmentItemMock = $this->getMockBuilder(ShipmentItemInterface::class) + ->getMock(); + $this->validator = $objectManagerHelper->getObject(QuantityValidator::class); + } + + public function testValidateTrackWithoutOrderId() + { + $this->shipmentMock->expects($this->once()) + ->method('getOrderId') + ->willReturn(null); + $this->assertEquals( + [__('Order Id is required for shipment document')], + $this->validator->validate($this->shipmentMock) + ); + } + + public function testValidateTrackWithoutItems() + { + $this->shipmentMock->expects($this->once()) + ->method('getOrderId') + ->willReturn(1); + $this->shipmentMock->expects($this->once()) + ->method('getItems') + ->willReturn(null); + $this->assertEquals( + [__('You can\'t create a shipment without products.')], + $this->validator->validate($this->shipmentMock) + ); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Validation/TrackValidatorTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Validation/TrackValidatorTest.php new file mode 100644 index 0000000000000..0d8d951ccf18a --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Validation/TrackValidatorTest.php @@ -0,0 +1,74 @@ +shipmentMock = $this->getMockBuilder(ShipmentInterface::class) + ->getMockForAbstractClass(); + $this->shipmentTrackMock = $this->getMockBuilder(ShipmentTrackInterface::class) + ->getMockForAbstractClass(); + $this->validator = $objectManagerHelper->getObject(TrackValidator::class); + } + + public function testValidateTrackWithNumber() + { + $this->shipmentTrackMock->expects($this->once()) + ->method('getTrackNumber') + ->willReturn('12345'); + $this->shipmentMock->expects($this->exactly(2)) + ->method('getTracks') + ->willReturn([$this->shipmentTrackMock]); + $this->assertEquals([], $this->validator->validate($this->shipmentMock)); + } + + public function testValidateTrackWithoutNumber() + { + $this->shipmentTrackMock->expects($this->once()) + ->method('getTrackNumber') + ->willReturn(null); + $this->shipmentMock->expects($this->exactly(2)) + ->method('getTracks') + ->willReturn([$this->shipmentTrackMock]); + $this->assertEquals([__('Please enter a tracking number.')], $this->validator->validate($this->shipmentMock)); + } + + public function testValidateTrackWithEmptyTracks() + { + $this->shipmentTrackMock->expects($this->never()) + ->method('getTrackNumber'); + $this->shipmentMock->expects($this->once()) + ->method('getTracks') + ->willReturn([]); + $this->assertEquals([], $this->validator->validate($this->shipmentMock)); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentDocumentFactoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentDocumentFactoryTest.php new file mode 100644 index 0000000000000..b0677b050f6fb --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentDocumentFactoryTest.php @@ -0,0 +1,195 @@ +shipmentFactoryMock = $this->getMockBuilder(ShipmentFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderMock = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->itemMock = $this->getMockBuilder(ShipmentItemCreationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->commentMock = $this->getMockBuilder(ShipmentCommentCreationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->shipmentMock = $this->getMockBuilder(ShipmentInterface::class) + ->disableOriginalConstructor() + ->setMethods(['addComment', 'addTrack']) + ->getMockForAbstractClass(); + + $this->hydratorPoolMock = $this->getMockBuilder(HydratorPool::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->trackFactoryMock = $this->getMockBuilder(TrackFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + + $this->trackMock = $this->getMockBuilder(Track::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->hydratorMock = $this->getMockBuilder(HydratorInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->shipmentDocumentFactory = new ShipmentDocumentFactory( + $this->shipmentFactoryMock, + $this->hydratorPoolMock, + $this->trackFactoryMock + ); + } + + public function testCreate() + { + $trackNum = "123456789"; + $trackData = [$trackNum]; + $tracks = [$this->trackMock]; + $appendComment = true; + $packages = []; + $items = [1 => 10]; + + $this->itemMock->expects($this->once()) + ->method('getOrderItemId') + ->willReturn(1); + + $this->itemMock->expects($this->once()) + ->method('getQty') + ->willReturn(10); + + $this->shipmentFactoryMock->expects($this->once()) + ->method('create') + ->with( + $this->orderMock, + $items + ) + ->willReturn($this->shipmentMock); + + $this->shipmentMock->expects($this->once()) + ->method('addTrack') + ->willReturnSelf(); + + $this->hydratorPoolMock->expects($this->once()) + ->method('getHydrator') + ->with(ShipmentTrackCreationInterface::class) + ->willReturn($this->hydratorMock); + + $this->hydratorMock->expects($this->once()) + ->method('extract') + ->with($this->trackMock) + ->willReturn($trackData); + + $this->trackFactoryMock->expects($this->once()) + ->method('create') + ->with(['data' => $trackData]) + ->willReturn($this->trackMock); + + if ($appendComment) { + $comment = "New comment!"; + $visibleOnFront = true; + $this->commentMock->expects($this->once()) + ->method('getComment') + ->willReturn($comment); + + $this->commentMock->expects($this->once()) + ->method('getIsVisibleOnFront') + ->willReturn($visibleOnFront); + + $this->shipmentMock->expects($this->once()) + ->method('addComment') + ->with($comment, $appendComment, $visibleOnFront) + ->willReturnSelf(); + } + + $this->assertEquals( + $this->shipmentDocumentFactory->create( + $this->orderMock, + [$this->itemMock], + $tracks, + $this->commentMock, + $appendComment, + $packages + ), + $this->shipmentMock + ); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentFactoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentFactoryTest.php index 3760934457a85..46d6ac62fc256 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentFactoryTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentFactoryTest.php @@ -5,10 +5,9 @@ */ namespace Magento\Sales\Test\Unit\Model\Order; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - /** * Unit test for shipment factory class. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ShipmentFactoryTest extends \PHPUnit_Framework_TestCase { @@ -39,7 +38,7 @@ class ShipmentFactoryTest extends \PHPUnit_Framework_TestCase */ protected function setUp() { - $objectManager = new ObjectManager($this); + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->converter = $this->getMock( \Magento\Sales\Model\Convert\Order::class, diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/OrderValidatorTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Validation/CanInvoiceTest.php similarity index 77% rename from app/code/Magento/Sales/Test/Unit/Model/Order/OrderValidatorTest.php rename to app/code/Magento/Sales/Test/Unit/Model/Order/Validation/CanInvoiceTest.php index 905d7c7a5b3f8..dd76bc1e52586 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/OrderValidatorTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Validation/CanInvoiceTest.php @@ -4,17 +4,17 @@ * See COPYING.txt for license details. */ -namespace Magento\Sales\Test\Unit\Model\Order; +namespace Magento\Sales\Test\Unit\Model\Order\Validation; use Magento\Sales\Model\Order; /** * Test for \Magento\Sales\Model\Order\OrderValidator class */ -class OrderValidatorTest extends \PHPUnit_Framework_TestCase +class CanInvoiceTest extends \PHPUnit_Framework_TestCase { /** - * @var \Magento\Sales\Model\Order\OrderValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Sales\Model\Order\Validation\CanInvoice|\PHPUnit_Framework_MockObject_MockObject */ private $model; @@ -47,7 +47,7 @@ protected function setUp() ->setMethods(['getQtyToInvoice', 'getLockedDoInvoice']) ->getMockForAbstractClass(); - $this->model = new \Magento\Sales\Model\Order\OrderValidator(); + $this->model = new \Magento\Sales\Model\Order\Validation\CanInvoice(); } /** @@ -62,9 +62,12 @@ public function testCanInvoiceWrongState($state) ->willReturn($state); $this->orderMock->expects($this->never()) ->method('getItems'); + $this->orderMock->expects($this->once()) + ->method('getStatus') + ->willReturn('status'); $this->assertEquals( - false, - $this->model->canInvoice($this->orderMock) + [__('An invoice cannot be created when an order has a status of %1', 'status')], + $this->model->validate($this->orderMock) ); } @@ -93,9 +96,8 @@ public function testCanInvoiceNoItems() ->method('getItems') ->willReturn([]); - $this->assertEquals( - false, - $this->model->canInvoice($this->orderMock) + $this->assertNotEmpty( + $this->model->validate($this->orderMock) ); } @@ -125,7 +127,7 @@ public function testCanInvoice($qtyToInvoice, $itemLockedDoInvoice, $expectedRes $this->assertEquals( $expectedResult, - $this->model->canInvoice($this->orderMock) + $this->model->validate($this->orderMock) ); } @@ -137,10 +139,10 @@ public function testCanInvoice($qtyToInvoice, $itemLockedDoInvoice, $expectedRes public function canInvoiceDataProvider() { return [ - [0, null, false], - [-1, null, false], - [1, true, false], - [0.5, false, true], + [0, null, [__('The order does not allow an invoice to be created.')]], + [-1, null, [__('The order does not allow an invoice to be created.')]], + [1, true, [__('The order does not allow an invoice to be created.')]], + [0.5, false, []], ]; } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Validation/CanShipTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Validation/CanShipTest.php new file mode 100644 index 0000000000000..11d99fbb9cced --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Validation/CanShipTest.php @@ -0,0 +1,146 @@ +objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->orderMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getStatus', 'getItems']) + ->getMockForAbstractClass(); + + $this->orderItemMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderItemInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getQtyToShip', 'getLockedDoShip']) + ->getMockForAbstractClass(); + + $this->model = new \Magento\Sales\Model\Order\Validation\CanShip(); + } + + /** + * @param string $state + * + * @dataProvider canShipWrongStateDataProvider + */ + public function testCanShipWrongState($state) + { + $this->orderMock->expects($this->any()) + ->method('getState') + ->willReturn($state); + $this->orderMock->expects($this->once()) + ->method('getStatus') + ->willReturn('status'); + $this->orderMock->expects($this->never()) + ->method('getItems'); + $this->assertEquals( + [__('A shipment cannot be created when an order has a status of %1', 'status')], + $this->model->validate($this->orderMock) + ); + } + + /** + * Data provider for testCanShipWrongState + * @return array + */ + public function canShipWrongStateDataProvider() + { + return [ + [Order::STATE_PAYMENT_REVIEW], + [Order::STATE_HOLDED], + [Order::STATE_CANCELED], + ]; + } + + public function testCanShipNoItems() + { + $this->orderMock->expects($this->any()) + ->method('getState') + ->willReturn(Order::STATE_PROCESSING); + + $this->orderMock->expects($this->once()) + ->method('getItems') + ->willReturn([]); + + $this->assertNotEmpty( + $this->model->validate($this->orderMock) + ); + } + + /** + * @param float $qtyToShipment + * @param bool|null $itemLockedDoShipment + * @param bool $expectedResult + * + * @dataProvider canShipDataProvider + */ + public function testCanShip($qtyToShipment, $itemLockedDoShipment, $expectedResult) + { + $this->orderMock->expects($this->any()) + ->method('getState') + ->willReturn(Order::STATE_PROCESSING); + + $items = [$this->orderItemMock]; + $this->orderMock->expects($this->once()) + ->method('getItems') + ->willReturn($items); + $this->orderItemMock->expects($this->any()) + ->method('getQtyToShip') + ->willReturn($qtyToShipment); + $this->orderItemMock->expects($this->any()) + ->method('getLockedDoShip') + ->willReturn($itemLockedDoShipment); + + $this->assertEquals( + $expectedResult, + $this->model->validate($this->orderMock) + ); + } + + /** + * Data provider for testCanShip + * + * @return array + */ + public function canShipDataProvider() + { + return [ + [0, null, [__('The order does not allow a shipment to be created.')]], + [-1, null, [__('The order does not allow a shipment to be created.')]], + [1, true, [__('The order does not allow a shipment to be created.')]], + [0.5, false, []], + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php new file mode 100644 index 0000000000000..b719babf209f0 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php @@ -0,0 +1,430 @@ +resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->shipmentDocumentFactoryMock = $this->getMockBuilder(ShipmentDocumentFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->shipmentValidatorMock = $this->getMockBuilder(ShipmentValidatorInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->orderValidatorMock = $this->getMockBuilder(OrderValidatorInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->orderRegistrarMock = $this->getMockBuilder(OrderRegistrarInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->orderStateResolverMock = $this->getMockBuilder(OrderStateResolverInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->configMock = $this->getMockBuilder(OrderConfig::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->shipmentRepositoryMock = $this->getMockBuilder(ShipmentRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->notifierInterfaceMock = $this->getMockBuilder(NotifierInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->shipmentCommentCreationMock = $this->getMockBuilder(ShipmentCommentCreationInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->shipmentCreationArgumentsMock = $this->getMockBuilder(ShipmentCreationArgumentsInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->shipmentMock = $this->getMockBuilder(ShipmentInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->packageMock = $this->getMockBuilder(ShipmentPackageInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->trackMock = $this->getMockBuilder(ShipmentTrackCreationInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->adapterMock = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->model = $helper->getObject( + ShipOrder::class, + [ + 'resourceConnection' => $this->resourceConnectionMock, + 'orderRepository' => $this->orderRepositoryMock, + 'shipmentRepository' => $this->shipmentRepositoryMock, + 'shipmentDocumentFactory' => $this->shipmentDocumentFactoryMock, + 'shipmentValidator' => $this->shipmentValidatorMock, + 'orderValidator' => $this->orderValidatorMock, + 'orderStateResolver' => $this->orderStateResolverMock, + 'orderRegistrar' => $this->orderRegistrarMock, + 'notifierInterface' => $this->notifierInterfaceMock, + 'config' => $this->configMock, + 'logger' => $this->loggerMock + ] + ); + } + + /** + * @dataProvider dataProvider + */ + public function testExecute($orderId, $items, $notify, $appendComment) + { + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->with('sales') + ->willReturn($this->adapterMock); + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + + $this->shipmentDocumentFactoryMock->expects($this->once()) + ->method('create') + ->with( + $this->orderMock, + $items, + [$this->trackMock], + $this->shipmentCommentCreationMock, + ($appendComment && $notify), + [$this->packageMock], + $this->shipmentCreationArgumentsMock + )->willReturn($this->shipmentMock); + + $this->shipmentValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->shipmentMock) + ->willReturn([]); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) + ->willReturn([]); + + $this->orderRegistrarMock->expects($this->once()) + ->method('register') + ->with($this->orderMock, $this->shipmentMock) + ->willReturn($this->orderMock); + + $this->orderStateResolverMock->expects($this->once()) + ->method('getStateForOrder') + ->with($this->orderMock, [OrderStateResolverInterface::IN_PROGRESS]) + ->willReturn(Order::STATE_PROCESSING); + + $this->orderMock->expects($this->once()) + ->method('setState') + ->with(Order::STATE_PROCESSING) + ->willReturnSelf(); + + $this->orderMock->expects($this->once()) + ->method('getState') + ->willReturn(Order::STATE_PROCESSING); + + $this->configMock->expects($this->once()) + ->method('getStateDefaultStatus') + ->with(Order::STATE_PROCESSING) + ->willReturn('Processing'); + + $this->orderMock->expects($this->once()) + ->method('setStatus') + ->with('Processing') + ->willReturnSelf(); + + $this->shipmentRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->shipmentMock) + ->willReturn($this->shipmentMock); + + $this->orderRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->orderMock) + ->willReturn($this->orderMock); + + if ($notify) { + $this->notifierInterfaceMock->expects($this->once()) + ->method('notify') + ->with($this->orderMock, $this->shipmentMock, $this->shipmentCommentCreationMock); + } + + $this->shipmentMock->expects($this->once()) + ->method('getEntityId') + ->willReturn(2); + + $this->assertEquals( + 2, + $this->model->execute( + $orderId, + $items, + $notify, + $appendComment, + $this->shipmentCommentCreationMock, + [$this->trackMock], + [$this->packageMock], + $this->shipmentCreationArgumentsMock + ) + ); + } + + /** + * @expectedException \Magento\Sales\Api\Exception\DocumentValidationExceptionInterface + */ + public function testDocumentValidationException() + { + $orderId = 1; + $items = [1 => 2]; + $notify = true; + $appendComment = true; + $errorMessages = ['error1', 'error2']; + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + + $this->shipmentDocumentFactoryMock->expects($this->once()) + ->method('create') + ->with( + $this->orderMock, + $items, + [$this->trackMock], + $this->shipmentCommentCreationMock, + ($appendComment && $notify), + [$this->packageMock], + $this->shipmentCreationArgumentsMock + )->willReturn($this->shipmentMock); + + $this->shipmentValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->shipmentMock) + ->willReturn($errorMessages); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) + ->willReturn([]); + + $this->model->execute( + $orderId, + $items, + $notify, + $appendComment, + $this->shipmentCommentCreationMock, + [$this->trackMock], + [$this->packageMock], + $this->shipmentCreationArgumentsMock + ); + } + + /** + * @expectedException \Magento\Sales\Api\Exception\CouldNotShipExceptionInterface + */ + public function testCouldNotInvoiceException() + { + $orderId = 1; + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->with('sales') + ->willReturn($this->adapterMock); + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + + $this->shipmentDocumentFactoryMock->expects($this->once()) + ->method('create') + ->with( + $this->orderMock + )->willReturn($this->shipmentMock); + + $this->shipmentValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->shipmentMock) + ->willReturn([]); + $this->orderValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->orderMock) + ->willReturn([]); + $e = new \Exception(); + + $this->orderRegistrarMock->expects($this->once()) + ->method('register') + ->with($this->orderMock, $this->shipmentMock) + ->willThrowException($e); + + $this->loggerMock->expects($this->once()) + ->method('critical') + ->with($e); + + $this->adapterMock->expects($this->once()) + ->method('rollBack'); + + $this->model->execute( + $orderId + ); + } + + /** + * @return array + */ + public function dataProvider() + { + return [ + 'TestWithNotifyTrue' => [1, [1 => 2], true, true], + 'TestWithNotifyFalse' => [1, [1 => 2], false, true], + ]; + } +} diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index 383650c8688fc..dfdb0f6a261c5 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -30,7 +30,9 @@ + + @@ -49,7 +51,10 @@ - + + + + @@ -62,13 +67,14 @@ + - + - + @@ -86,10 +92,14 @@ - - - + + + + + + + @@ -909,4 +919,18 @@ + + + + Magento\Sales\Model\Order\Shipment\Sender\EmailSender + + + + + + + Magento\Framework\EntityManager\HydratorInterface + + + diff --git a/app/code/Magento/Sales/etc/webapi.xml b/app/code/Magento/Sales/etc/webapi.xml index 8d1b1fda5bc31..4c7fe03a201f8 100644 --- a/app/code/Magento/Sales/etc/webapi.xml +++ b/app/code/Magento/Sales/etc/webapi.xml @@ -235,6 +235,12 @@ + + + + + + @@ -254,7 +260,7 @@ - + diff --git a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php index adbd96624649d..d265159bc630b 100644 --- a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php +++ b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php @@ -7,8 +7,12 @@ namespace Magento\Shipping\Controller\Adminhtml\Order\Shipment; use Magento\Backend\App\Action; -use Magento\Sales\Model\Order\Email\Sender\ShipmentSender; +use Magento\Sales\Model\Order\Shipment\Validation\QuantityValidator; +/** + * Class Save + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Save extends \Magento\Backend\App\Action { /** @@ -29,21 +33,26 @@ class Save extends \Magento\Backend\App\Action protected $labelGenerator; /** - * @var ShipmentSender + * @var \Magento\Sales\Model\Order\Email\Sender\ShipmentSender */ protected $shipmentSender; /** - * @param Action\Context $context + * @var \Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface + */ + private $shipmentValidator; + + /** + * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader $shipmentLoader * @param \Magento\Shipping\Model\Shipping\LabelGenerator $labelGenerator - * @param ShipmentSender $shipmentSender + * @param \Magento\Sales\Model\Order\Email\Sender\ShipmentSender $shipmentSender */ public function __construct( - Action\Context $context, + \Magento\Backend\App\Action\Context $context, \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader $shipmentLoader, \Magento\Shipping\Model\Shipping\LabelGenerator $labelGenerator, - ShipmentSender $shipmentSender + \Magento\Sales\Model\Order\Email\Sender\ShipmentSender $shipmentSender ) { $this->shipmentLoader = $shipmentLoader; $this->labelGenerator = $labelGenerator; @@ -119,7 +128,14 @@ public function execute() $shipment->setCustomerNote($data['comment_text']); $shipment->setCustomerNoteNotify(isset($data['comment_customer_notify'])); } - + $errorMessages = $this->getShipmentValidator()->validate($shipment, [QuantityValidator::class]); + if (!empty($errorMessages)) { + $this->messageManager->addError( + __("Shipment Document Validation Error(s):\n" . implode("\n", $errorMessages)) + ); + $this->_redirect('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); + return; + } $shipment->register(); $shipment->getOrder()->setCustomerNoteNotify(!empty($data['send_email'])); @@ -168,4 +184,19 @@ public function execute() $this->_redirect('sales/order/view', ['order_id' => $shipment->getOrderId()]); } } + + /** + * @return \Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface + * @deprecated + */ + private function getShipmentValidator() + { + if ($this->shipmentValidator === null) { + $this->shipmentValidator = $this->_objectManager->get( + \Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface::class + ); + } + + return $this->shipmentValidator; + } } diff --git a/app/code/Magento/Shipping/Controller/Adminhtml/Order/ShipmentLoader.php b/app/code/Magento/Shipping/Controller/Adminhtml/Order/ShipmentLoader.php index b452c88887c9e..c4efe6f6507d5 100644 --- a/app/code/Magento/Shipping/Controller/Adminhtml/Order/ShipmentLoader.php +++ b/app/code/Magento/Shipping/Controller/Adminhtml/Order/ShipmentLoader.php @@ -1,6 +1,5 @@ method('getFormKeyValidator') ->will($this->returnValue($this->formKeyValidator)); + $this->shipmentValidatorMock = $this->getMockBuilder(ShipmentValidatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->saveAction = $objectManagerHelper->getObject( \Magento\Shipping\Controller\Adminhtml\Order\Shipment\Save::class, [ @@ -218,7 +230,8 @@ protected function setUp() 'context' => $this->context, 'shipmentLoader' => $this->shipmentLoader, 'request' => $this->request, - 'response' => $this->response + 'response' => $this->response, + 'shipmentValidator' => $this->shipmentValidatorMock ] ); } @@ -346,6 +359,11 @@ public function testExecute($formKeyIsValid, $isPost) ->will($this->returnValue($orderId)); $this->prepareRedirect($path, $arguments); + $this->shipmentValidatorMock->expects($this->once()) + ->method('validate') + ->with($shipment, [QuantityValidator::class]) + ->willReturn([]); + $this->saveAction->execute(); $this->assertEquals($this->response, $this->saveAction->getResponse()); } diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderInvoiceCreateTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderInvoiceCreateTest.php index cb384134a7c68..60c9f54ea132c 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderInvoiceCreateTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderInvoiceCreateTest.php @@ -10,7 +10,7 @@ */ class OrderInvoiceCreateTest extends \Magento\TestFramework\TestCase\WebapiAbstract { - const SERVICE_READ_NAME = 'salesOrderInvoiceV1'; + const SERVICE_READ_NAME = 'salesInvoiceOrderV1'; const SERVICE_VERSION = 'V1'; /** diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php new file mode 100644 index 0000000000000..8de7c4dc7f65b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php @@ -0,0 +1,100 @@ +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + $this->shipmentRepository = $this->objectManager->get( + \Magento\Sales\Api\ShipmentRepositoryInterface::class + ); + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/order_new.php + */ + public function testShipOrder() + { + /** @var \Magento\Sales\Model\Order $existingOrder */ + $existingOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/order/' . $existingOrder->getId() . '/ship', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => self::SERVICE_READ_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_READ_NAME . 'execute', + ], + ]; + + $requestData = [ + 'orderId' => $existingOrder->getId(), + 'items' => [], + 'comment' => [ + 'comment' => 'Test Comment', + 'is_visible_on_front' => 1, + ], + 'tracks' => [ + [ + 'track_number' => 'TEST_TRACK_0001', + 'title' => 'Simple shipment track', + 'carrier_code' => 'UPS' + ] + ] + ]; + + /** @var \Magento\Sales\Api\Data\OrderItemInterface $item */ + foreach ($existingOrder->getAllItems() as $item) { + $requestData['items'][] = [ + 'order_item_id' => $item->getItemId(), + 'qty' => $item->getQtyOrdered(), + ]; + } + + $result = $this->_webApiCall($serviceInfo, $requestData); + + $this->assertNotEmpty($result); + + try { + $this->shipmentRepository->get($result); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + $this->fail('Failed asserting that Shipment was created'); + } + + /** @var \Magento\Sales\Model\Order $updatedOrder */ + $updatedOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + + $this->assertNotEquals( + $existingOrder->getStatus(), + $updatedOrder->getStatus(), + 'Failed asserting that Order status was changed' + ); + } +} diff --git a/lib/internal/Magento/Framework/EntityManager/CustomAttributesMapper.php b/lib/internal/Magento/Framework/EntityManager/CustomAttributesMapper.php index 9147d47f3d9dd..fe3a199da86a3 100644 --- a/lib/internal/Magento/Framework/EntityManager/CustomAttributesMapper.php +++ b/lib/internal/Magento/Framework/EntityManager/CustomAttributesMapper.php @@ -55,8 +55,9 @@ public function __construct( */ public function entityToDatabase($entityType, $data) { - $metadata = $this->metadataPool->getMetadata($entityType); - if (!$metadata->getEavEntityType()) { + if (!$this->metadataPool->hasConfiguration($entityType) + || !$this->metadataPool->getMetadata($entityType)->getEavEntityType() + ) { return $data; } if (isset($data[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES])) { diff --git a/lib/internal/Magento/Framework/EntityManager/Test/Unit/CustomAttributesMapperTest.php b/lib/internal/Magento/Framework/EntityManager/Test/Unit/CustomAttributesMapperTest.php index a39ad4afaa6ee..56977af1dd6eb 100644 --- a/lib/internal/Magento/Framework/EntityManager/Test/Unit/CustomAttributesMapperTest.php +++ b/lib/internal/Magento/Framework/EntityManager/Test/Unit/CustomAttributesMapperTest.php @@ -48,12 +48,18 @@ public function testEntityToDatabase() $metadataPool = $this->getMockBuilder(\Magento\Framework\EntityManager\MetadataPool::class) ->disableOriginalConstructor() - ->setMethods(['getMetadata']) + ->setMethods(['getMetadata', 'hasConfiguration']) ->getMock(); + $metadataPool->expects($this->any()) + ->method('hasConfiguration') + ->willReturn(true); $metadataPool->expects($this->any()) ->method('getMetadata') ->with($this->equalTo(\Magento\Customer\Api\Data\AddressInterface::class)) ->will($this->returnValue($metadata)); + $metadataPool->expects($this->once()) + ->method('hasConfiguration') + ->willReturn(true); $searchCriteriaBuilder = $this->getMockBuilder(\Magento\Framework\Api\SearchCriteriaBuilder::class) ->disableOriginalConstructor() @@ -76,6 +82,7 @@ public function testEntityToDatabase() 'metadataPool' => $metadataPool, 'searchCriteriaBuilder' => $searchCriteriaBuilder ]); + $actual = $customAttributesMapper->entityToDatabase( \Magento\Customer\Api\Data\AddressInterface::class, [ diff --git a/lib/internal/Magento/Framework/EntityManager/TypeResolver.php b/lib/internal/Magento/Framework/EntityManager/TypeResolver.php index 28e2bdaa70942..2718162e80d66 100644 --- a/lib/internal/Magento/Framework/EntityManager/TypeResolver.php +++ b/lib/internal/Magento/Framework/EntityManager/TypeResolver.php @@ -20,7 +20,8 @@ class TypeResolver */ private $typeMapping = [ \Magento\SalesRule\Model\Rule::class => \Magento\SalesRule\Api\Data\RuleInterface::class, - \Magento\SalesRule\Model\Rule\Interceptor::class => \Magento\SalesRule\Api\Data\RuleInterface::class + \Magento\SalesRule\Model\Rule\Interceptor::class => \Magento\SalesRule\Api\Data\RuleInterface::class, + \Magento\SalesRule\Model\Rule\Proxy::class => \Magento\SalesRule\Api\Data\RuleInterface::class ]; /** @@ -50,8 +51,7 @@ public function resolve($type) $dataInterfaces = []; foreach ($interfaceNames as $interfaceName) { if (strpos($interfaceName, '\Api\Data\\')) { - $dataInterfaces[] = isset($this->config[$interfaceName]) - ? $this->config[$interfaceName] : $interfaceName; + $dataInterfaces[] = $interfaceName; } } @@ -64,7 +64,9 @@ public function resolve($type) $this->typeMapping[$className] = $dataInterface; } } - + if (empty($this->typeMapping[$className])) { + $this->typeMapping[$className] = reset($dataInterfaces); + } return $this->typeMapping[$className]; } }