InvoiceService.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Sales\Model\Service;
  7. use Magento\Sales\Api\InvoiceManagementInterface;
  8. use Magento\Sales\Model\Order;
  9. use Magento\Framework\App\ObjectManager;
  10. use Magento\Framework\Serialize\Serializer\Json;
  11. use Magento\Catalog\Model\Product\Type;
  12. /**
  13. * Class InvoiceService
  14. *
  15. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  16. */
  17. class InvoiceService implements InvoiceManagementInterface
  18. {
  19. /**
  20. * Repository
  21. *
  22. * @var \Magento\Sales\Api\InvoiceRepositoryInterface
  23. */
  24. protected $repository;
  25. /**
  26. * Repository
  27. *
  28. * @var \Magento\Sales\Api\InvoiceCommentRepositoryInterface
  29. */
  30. protected $commentRepository;
  31. /**
  32. * Search Criteria Builder
  33. *
  34. * @var \Magento\Framework\Api\SearchCriteriaBuilder
  35. */
  36. protected $criteriaBuilder;
  37. /**
  38. * Filter Builder
  39. *
  40. * @var \Magento\Framework\Api\FilterBuilder
  41. */
  42. protected $filterBuilder;
  43. /**
  44. * Invoice Notifier
  45. *
  46. * @var \Magento\Sales\Model\Order\InvoiceNotifier
  47. */
  48. protected $invoiceNotifier;
  49. /**
  50. * @var \Magento\Sales\Api\OrderRepositoryInterface
  51. */
  52. protected $orderRepository;
  53. /**
  54. * @var \Magento\Sales\Model\Convert\Order
  55. */
  56. protected $orderConverter;
  57. /**
  58. * Serializer interface instance.
  59. *
  60. * @var Json
  61. */
  62. private $serializer;
  63. /**
  64. * Constructor
  65. *
  66. * @param \Magento\Sales\Api\InvoiceRepositoryInterface $repository
  67. * @param \Magento\Sales\Api\InvoiceCommentRepositoryInterface $commentRepository
  68. * @param \Magento\Framework\Api\SearchCriteriaBuilder $criteriaBuilder
  69. * @param \Magento\Framework\Api\FilterBuilder $filterBuilder
  70. * @param \Magento\Sales\Model\Order\InvoiceNotifier $notifier
  71. * @param \Magento\Sales\Api\OrderRepositoryInterface $orderRepository
  72. * @param \Magento\Sales\Model\Convert\Order $orderConverter
  73. * @param Json|null $serializer
  74. */
  75. public function __construct(
  76. \Magento\Sales\Api\InvoiceRepositoryInterface $repository,
  77. \Magento\Sales\Api\InvoiceCommentRepositoryInterface $commentRepository,
  78. \Magento\Framework\Api\SearchCriteriaBuilder $criteriaBuilder,
  79. \Magento\Framework\Api\FilterBuilder $filterBuilder,
  80. \Magento\Sales\Model\Order\InvoiceNotifier $notifier,
  81. \Magento\Sales\Api\OrderRepositoryInterface $orderRepository,
  82. \Magento\Sales\Model\Convert\Order $orderConverter,
  83. Json $serializer = null
  84. ) {
  85. $this->repository = $repository;
  86. $this->commentRepository = $commentRepository;
  87. $this->criteriaBuilder = $criteriaBuilder;
  88. $this->filterBuilder = $filterBuilder;
  89. $this->invoiceNotifier = $notifier;
  90. $this->orderRepository = $orderRepository;
  91. $this->orderConverter = $orderConverter;
  92. $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
  93. }
  94. /**
  95. * @inheritdoc
  96. */
  97. public function setCapture($id)
  98. {
  99. return (bool)$this->repository->get($id)->capture();
  100. }
  101. /**
  102. * @inheritdoc
  103. */
  104. public function getCommentsList($id)
  105. {
  106. $this->criteriaBuilder->addFilters(
  107. [$this->filterBuilder->setField('parent_id')->setValue($id)->setConditionType('eq')->create()]
  108. );
  109. $searchCriteria = $this->criteriaBuilder->create();
  110. return $this->commentRepository->getList($searchCriteria);
  111. }
  112. /**
  113. * @inheritdoc
  114. */
  115. public function notify($id)
  116. {
  117. $invoice = $this->repository->get($id);
  118. return $this->invoiceNotifier->notify($invoice);
  119. }
  120. /**
  121. * @inheritdoc
  122. */
  123. public function setVoid($id)
  124. {
  125. return (bool)$this->repository->get($id)->void();
  126. }
  127. /**
  128. * Creates an invoice based on the order and quantities provided
  129. *
  130. * @param Order $order
  131. * @param array $qtys
  132. * @return \Magento\Sales\Model\Order\Invoice
  133. * @throws \Magento\Framework\Exception\LocalizedException
  134. */
  135. public function prepareInvoice(Order $order, array $qtys = [])
  136. {
  137. $invoice = $this->orderConverter->toInvoice($order);
  138. $totalQty = 0;
  139. $qtys = $this->prepareItemsQty($order, $qtys);
  140. foreach ($order->getAllItems() as $orderItem) {
  141. if (!$this->_canInvoiceItem($orderItem, $qtys)) {
  142. continue;
  143. }
  144. $item = $this->orderConverter->itemToInvoiceItem($orderItem);
  145. if (isset($qtys[$orderItem->getId()])) {
  146. $qty = (double) $qtys[$orderItem->getId()];
  147. } elseif ($orderItem->isDummy()) {
  148. $qty = $orderItem->getQtyOrdered() ? $orderItem->getQtyOrdered() : 1;
  149. } elseif (empty($qtys)) {
  150. $qty = $orderItem->getQtyToInvoice();
  151. } else {
  152. $qty = 0;
  153. }
  154. $totalQty += $qty;
  155. $this->setInvoiceItemQuantity($item, $qty);
  156. $invoice->addItem($item);
  157. }
  158. $invoice->setTotalQty($totalQty);
  159. $invoice->collectTotals();
  160. $order->getInvoiceCollection()->addItem($invoice);
  161. return $invoice;
  162. }
  163. /**
  164. * Prepare qty to invoice for parent and child products if theirs qty is not specified in initial request.
  165. *
  166. * @param Order $order
  167. * @param array $qtys
  168. * @return array
  169. */
  170. private function prepareItemsQty(Order $order, array $qtys = [])
  171. {
  172. foreach ($order->getAllItems() as $orderItem) {
  173. if (empty($qtys[$orderItem->getId()])) {
  174. if ($orderItem->getProductType() == Type::TYPE_BUNDLE && !$orderItem->isShipSeparately()) {
  175. $qtys[$orderItem->getId()] = $orderItem->getQtyOrdered() - $orderItem->getQtyInvoiced();
  176. } else {
  177. $parentItem = $orderItem->getParentItem();
  178. $parentItemId = $parentItem ? $parentItem->getId() : null;
  179. if ($parentItemId && isset($qtys[$parentItemId])) {
  180. $qtys[$orderItem->getId()] = $qtys[$parentItemId];
  181. }
  182. continue;
  183. }
  184. }
  185. $this->prepareItemQty($orderItem, $qtys);
  186. }
  187. return $qtys;
  188. }
  189. /**
  190. * Prepare qty_invoiced for order item
  191. *
  192. * @param \Magento\Sales\Api\Data\OrderItemInterface $orderItem
  193. * @param array $qtys
  194. */
  195. private function prepareItemQty(\Magento\Sales\Api\Data\OrderItemInterface $orderItem, &$qtys)
  196. {
  197. $this->prepareBundleQty($orderItem, $qtys);
  198. if ($orderItem->isDummy()) {
  199. if ($orderItem->getHasChildren()) {
  200. foreach ($orderItem->getChildrenItems() as $child) {
  201. if (!isset($qtys[$child->getId()])) {
  202. $qtys[$child->getId()] = $child->getQtyToInvoice();
  203. }
  204. $parentId = $orderItem->getParentItemId();
  205. if ($parentId && array_key_exists($parentId, $qtys)) {
  206. $qtys[$orderItem->getId()] = $qtys[$parentId];
  207. } else {
  208. continue;
  209. }
  210. }
  211. } elseif ($orderItem->getParentItem()) {
  212. $parent = $orderItem->getParentItem();
  213. if (!isset($qtys[$parent->getId()])) {
  214. $qtys[$parent->getId()] = $parent->getQtyToInvoice();
  215. }
  216. }
  217. }
  218. }
  219. /**
  220. * Prepare qty to invoice for bundle products
  221. *
  222. * @param \Magento\Sales\Api\Data\OrderItemInterface $orderItem
  223. * @param array $qtys
  224. */
  225. private function prepareBundleQty(\Magento\Sales\Api\Data\OrderItemInterface $orderItem, &$qtys)
  226. {
  227. if ($orderItem->getProductType() == Type::TYPE_BUNDLE && !$orderItem->isShipSeparately()) {
  228. foreach ($orderItem->getChildrenItems() as $childItem) {
  229. $bundleSelectionAttributes = $childItem->getProductOptionByCode('bundle_selection_attributes');
  230. if (is_string($bundleSelectionAttributes)) {
  231. $bundleSelectionAttributes = $this->serializer->unserialize($bundleSelectionAttributes);
  232. }
  233. $qtys[$childItem->getId()] = $qtys[$orderItem->getId()] * $bundleSelectionAttributes['qty'];
  234. }
  235. }
  236. }
  237. /**
  238. * Check if order item can be invoiced.
  239. *
  240. * @param \Magento\Sales\Api\Data\OrderItemInterface $item
  241. * @param array $qtys
  242. * @return bool
  243. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  244. */
  245. protected function _canInvoiceItem(\Magento\Sales\Api\Data\OrderItemInterface $item, array $qtys = [])
  246. {
  247. if ($item->getLockedDoInvoice()) {
  248. return false;
  249. }
  250. if ($item->isDummy()) {
  251. if ($item->getHasChildren()) {
  252. foreach ($item->getChildrenItems() as $child) {
  253. if (empty($qtys)) {
  254. if ($child->getQtyToInvoice() > 0) {
  255. return true;
  256. }
  257. } else {
  258. if (isset($qtys[$child->getId()]) && $qtys[$child->getId()] > 0) {
  259. return true;
  260. }
  261. }
  262. }
  263. return false;
  264. } elseif ($item->getParentItem()) {
  265. $parent = $item->getParentItem();
  266. if (empty($qtys)) {
  267. return $parent->getQtyToInvoice() > 0;
  268. } else {
  269. return isset($qtys[$parent->getId()]) && $qtys[$parent->getId()] > 0;
  270. }
  271. }
  272. } else {
  273. return $item->getQtyToInvoice() > 0;
  274. }
  275. }
  276. /**
  277. * Set quantity to invoice item
  278. *
  279. * @param \Magento\Sales\Api\Data\InvoiceItemInterface $item
  280. * @param float $qty
  281. * @return $this
  282. * @throws \Magento\Framework\Exception\LocalizedException
  283. */
  284. protected function setInvoiceItemQuantity(\Magento\Sales\Api\Data\InvoiceItemInterface $item, $qty)
  285. {
  286. $qty = ($item->getOrderItem()->getIsQtyDecimal()) ? (double) $qty : (int) $qty;
  287. $qty = $qty > 0 ? $qty : 0;
  288. /**
  289. * Check qty availability
  290. */
  291. $qtyToInvoice = sprintf("%F", $item->getOrderItem()->getQtyToInvoice());
  292. $qty = sprintf("%F", $qty);
  293. if ($qty > $qtyToInvoice && !$item->getOrderItem()->isDummy()) {
  294. throw new \Magento\Framework\Exception\LocalizedException(
  295. __('We found an invalid quantity to invoice item "%1".', $item->getName())
  296. );
  297. }
  298. $item->setQty($qty);
  299. return $this;
  300. }
  301. }