logger = $logger; $this->orderCollection = $orderCollection; $this->cartRepository = $cartRepository; $this->getPickupLocationCode = $getPickupLocationCode; $this->getList = $getList; $this->productRepository = $productRepository; $this->netSuiteAPI = $netSuiteAPI; $this->orderRepository = $orderRepository; $this->sourceItemRepository = $sourceItemRepository; $this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory; $this->addressInterfaceFactory = $addressInterfaceFactory; $this->getDistanceFromSourceToAddress = $getDistanceFromSourceToAddress; $this->customerRepository = $customerRepository; $this->magentoLogger = $magentoLogger; $this->getInventorySources(); } /** * @return array */ protected function getInventorySources() { if (empty($this->sources)) { foreach ($this->getList->execute()->getItems() as $item) { $this->sources[$item->getSourceCode()] = $item; } } return $this->sources; } /** * @throws LocalizedException */ protected function getCardInfo() { $payment = $this->order->getPayment(); $billingAddress = $this->order->getBillingAddress(); $paymentMethod = $payment->getMethodInstance(); if ($paymentMethod->isOffline()) { return ''; } return $payment->getCcType().': '.$payment->getCcLast4().', '. $billingAddress->getFirstname().' '.$billingAddress->getLastname().': '. implode(' ', $billingAddress->getStreet()).', '. $billingAddress->getCity().', '.$billingAddress->getRegion() .', '.$billingAddress->getPostcode(); } protected function getProductNetsuiteId(int $productId) { try { $product = $this->productRepository->getById($productId); return $product->getNetsuiteId(); } catch (NoSuchEntityException $e) { } return null; } /** * @param string $pickupLocationCode * @return array */ protected function getNetsuiteOrderItems(string $pickupLocationCode) { $data = []; if (!empty($pickupLocationCode)) { foreach ($this->order->getAllVisibleItems() as $item) { /* @var $item OrderItemInterface */ $data[$item->getItemId()] = [ 'item' => ['id' => $this->getProductNetsuiteId((int)$item->getProductId())], 'rate' => $this->numberFormat($item->getPrice()), 'sku' => $item->getSku(), 'quantity' => $item->getQtyOrdered()*1.0 ]; } } return $data; } /** * 拆单规则: * 在checkout的时候,如果客户选择ship out,我们首先看哪个hub有相对应的库存,如果每一个hub都有货,那我们就根据shipping address的 * region来确定哪个hub出货。譬如旧金山下单了,三个hub都有货,那我们优先从西部的hub(洛杉矶)出货。假如说三个hub都没货,那我们就优先从 * 有货的warehouse出,如果有多个仓库有货的话还是按照region来。 * * 如果没有一个仓库能满足整张单的库存量的话,那我们就会拆单。具体的选择仓库或者hub的逻辑还是按照上面的那个来,哪个能fulfill的多的就从哪 * 一个出,如果有多个的话就距离优先。如果可以的话,可以在M2就拆成两张单吗?税的话应该怎么拆都不会变,因为都是按照销售的物品来的。运费的话 * 我们是这样子想的,如果拆成两张单,我们只会收客户两张单里面运费高的那一份运费,剩下的运费我们自己承担。 * * hub的话是指我们三个大仓库,分别是LA,TX,NJ, * * 如果客户选择pickup的话,如果他选择的preferred warehouse里面有库存的话那就直接可以process;如果没有的话,我们就会提示客户说当前物 * 品在你preferred warehouse没有库存,你可以选择配送或者把缺货商品从购物车里移除 * * (1) 关于订单同步到netsuite时的拆单 * (1.1)根据上述规则拆单 * (1.2)运费可以放在其中一张单上 * (1.3)订单金额根据拆单后的产品的金额计算 * (1.4)税可以不用给 * * @return array */ protected function getSyncOrders() { /* @var $info string | null */ $information = $this->order->getData('additional_information'); if ($information) { $info = json_decode($information, true); if (is_array($info) && isset($info['syncOrders'])) { return $info['syncOrders']; } } $pickupLocationCode = $this->getPickupLocationCode->execute($this->order); if ($pickupLocationCode) { return [$pickupLocationCode => $this->getNetsuiteOrderItems($pickupLocationCode)]; } return $this->orderSplit(); } /** * @param string|float $number * @return float */ protected function numberFormat(string | float $number) { return number_format($number*1.0, 2, '.', '')*1.0; } /** * order split * @return array */ protected function orderSplit() { $result = []; foreach ($this->order->getAllVisibleItems() as $item) { /* @var $item OrderItemInterface */ echo sprintf('order %s item id %d', $this->order->getIncrementId(), $item->getItemId()); foreach ($this->getInventorySourceFromProduct($item) as $sourceCode =>$splitItem) { $result[$sourceCode][$item->getItemId()] = [ 'item' => ['id' => $this->getProductNetsuiteId((int)$item->getProductId())], 'rate' => $this->numberFormat($item->getPrice()), 'sku' => $item->getSku(), 'quantity' => $splitItem[$item->getItemId()]*1.0 ]; } } return $result; } /** * @param OrderItemInterface $orderItem * @return float[][] */ protected function getInventorySourceFromProduct(OrderItemInterface $orderItem) { $searchCriteria = $this->searchCriteriaBuilderFactory->create() ->addFilter(SourceItemInterface::SKU, $orderItem->getSku()) ->addFilter(SourceItemInterface::SOURCE_CODE, self::DEFAULT_INVENTORY_SOURCE, 'neq') ->create(); $sourceItems = $this->sourceItemRepository->getList($searchCriteria)->getItems(); $availableItems = []; $distances = []; $oneSource = false; foreach ($sourceItems as $sourceItem) { $sourceAddress = $this->addressTransform($this->order->getShippingAddress()); $distances[$sourceItem->getSourceCode()] = $distance = $this->getDistanceFromSourceToAddress->execute( $this->sources[$sourceItem->getSourceCode()], $sourceAddress ); if ($sourceItem->getQuantity() >= $orderItem->getQtyOrdered()) { $oneSource = true; $availableItems[$distance] = [ 'sourceItem' => $sourceItem, 'isHub' => in_array($sourceItem->getSourceCode(), self::HUB_REPOSITORY_CODE), ]; } } //one warehouse inventory is not enough for this order product if (empty($availableItems)) { foreach ($sourceItems as $sourceItem) { $availableItems[$distances[$sourceItem->getSourceCode()]] = [ 'sourceItem' => $sourceItem, 'isHub' => in_array($sourceItem->getSourceCode(), self::HUB_REPOSITORY_CODE) ]; } } ksort($availableItems); // $warehouseDistances = []; // foreach ($availableItems as $distance => $items) { // $warehouseDistances[] = [ // 'product'=>$orderItem->getSku(), // 'warehouse code'=>$items['sourceItem']->getSourceCode(), // 'warehouse_stock' => $items['sourceItem']->getQuantity(), // 'distance'=>$distance, // 'isHub' => $items['isHub'] // ]; // } // $this->logger->debug('distance on warehouse: ', $warehouseDistances); return $this->getPreferredInventorySource($availableItems, $orderItem, $oneSource); } /** * choose preferred warehouse for ordered product */ protected function getPreferredInventorySource(array $availableItems, OrderItemInterface $orderItem, bool $oneSource) { $hub = []; $other = []; $qty = $orderItem->getQtyOrdered(); foreach ($availableItems as $availableItem) { if ($availableItem['isHub']) { $hub[] = $availableItem; } else { $other[] = $availableItem; } } //if one warehouse stock is enough for this ordered product, then return if ($oneSource === true) { $source = !empty($hub)? current($hub):current($other); // $source['qty'] = $qty; return [$source['sourceItem']->getSourceCode() => [$orderItem->getItemId() => $qty]]; } $aggregation = []; $total = 0; foreach ($availableItems as $item) { if ($total < $qty) { $aggregation[$item['sourceItem']->getSourceCode()] = $item['sourceItem']->getQuantity(); } $total += $item['sourceItem']->getQuantity(); } return $this->correctLastWarehouseQty($aggregation, $orderItem); } /** * @param array $availableItems * @param OrderItemInterface $orderItem * @return array */ protected function correctLastWarehouseQty(array $availableItems, OrderItemInterface $orderItem) { $lastKey = array_key_last($availableItems); $qty = $orderItem->getQtyOrdered(); $lastQty = $qty; $data = []; foreach ($availableItems as $sourceCode => $qty) { if ($sourceCode != $lastKey) { $lastQty -= $qty; } else { $availableItems[$lastKey] = $lastQty; } $data[$sourceCode][$orderItem->getItemId()] = $availableItems[$sourceCode]; } return $data; } /** * transform order address to the calculate distance address * @param \Magento\Sales\Api\Data\OrderAddressInterface $address * @return \Magento\InventorySourceSelectionApi\Api\Data\AddressInterface */ protected function addressTransform(\Magento\Sales\Api\Data\OrderAddressInterface $address) { if ($address->getStreet()) { $streets = implode(' ', $address->getStreet()); } return $this->addressInterfaceFactory->create([ 'country' => $address->getCountryId() ?? '', 'postcode' => $address->getPostcode() ?? '', 'street' => $streets ?? '', 'region' => $address->getRegion() ?? '', 'city' => $address->getCity() ?? '' ]); } protected function getShippingAddress() { $shippingAddress = $this->order->getShippingAddress(); $streets = ['', '']; if ($shippingAddress->getStreet()) { $streets = $shippingAddress->getStreet(); } return [ 'country' => ['id' => 'US'], 'addr1' => $streets[0], 'addr2' => $streets[1] ?? '', 'city' => $shippingAddress->getCity(), 'state' => $shippingAddress->getRegionCode(), 'zip' => $shippingAddress->getPostcode(), 'addressee' => $shippingAddress->getFirstname() . ' ' . $shippingAddress->getLastname(), 'attention' => null, 'addrPhone' => $shippingAddress->getTelephone(), 'override' => false ]; } /** * @return string * @throws NoSuchEntityException */ protected function getTerms() { $payment = $this->order->getPayment(); if ($payment->getMethod() == NetTerm::PAYMENT_METHOD_NETTERM_CODE) { return $this->getQuote()->getNetsuitePaymentId(); } return self::NETSUITE_CREDIT_CARD_PAYMENT_ID; } protected function getCustomer() { if ($this->customer) { return $this->customer; } if ($this->order->getCustomer()) { $this->customer = $this->order->getCustomer()->getDataModel(); } $this->customer = $this->customerRepository->getById($this->order->getCustomerId()); return $this->customer; } protected function getPostData(string $locationId, array $items, int $index) { $dataCustomer = $this->getCustomer(); $shippingAddress = $this->order->getShippingAddress(); $shippingCost = 0; if ($index == 1) { $shippingCost = $this->numberFormat($this->order->getShippingAmount()); } foreach ($items as &$item) { unset($item['sku']); } return [ 'externalId'=> 'Magento-'.$this->order->getIncrementId().'-'.$index, 'entity' => [ 'id'=>$dataCustomer->getCustomAttribute('netsuite_id')->getValue(), ], 'terms' => ['id' => $this->getTerms()], 'otherRefNum' => $this->getQuote()->getOtherRefNum(), 'custbody_il_email' => $shippingAddress->getEmail(), 'custbody_il_phone' => $shippingAddress->getTelephone(), 'custbody_card_info' => $this->getCardInfo(), 'custbody_ecommerce_order_no' => 'Magento-'.$this->order->getIncrementId().'-'.$index, 'subsidiary' => ['id'=> 3], 'cseg_ncg_rep_office' => ['id'=> 13], 'location' => ['id'=> $locationId], 'custbody_location_filter' => 'RMA', // 'discountItem' => ['id'=> -6], // 'discountRate' => 6.17, 'shipMethod' => ['id'=> ShippingMethodMapping::getShippingMethodId($this->order->getShippingMethod())], 'shippingCost' => $shippingCost, 'shipAddressList' => ['id'=>-2], 'shipIsResidential' => false, 'shippingAddress' => $this->getShippingAddress(), 'item' => ['items'=>$items], ]; } /** * @return CartInterface * @throws NoSuchEntityException */ protected function getQuote() { if (!$this->quote) { $this->quote = $this->cartRepository->get($this->order->getQuoteId()); } return $this->quote; } /** * @return \Magento\Sales\Api\Data\OrderSearchResultInterface */ protected function getWaitSyncOrders() { $criteria = $this->searchCriteriaBuilderFactory->create() ->addFilter('is_netsuite_sync', 0) ->addFilter('entity_id', [17173], 'in') ->create(); return $this->orderRepository->getList($criteria); } protected function customerDepositBuild() { $dataCustomer = $this->getCustomer(); return [ 'externalId'=> 'CD-'.$this->order->getIncrementId().'-1', 'customer' => ['id' => $dataCustomer->getCustomAttribute('netsuite_id')->getValue()], 'salesOrder' => ['externalId' => 'Magento-'.$this->order->getIncrementId().'-1'], 'payment' => $this->numberFormat($this->order->getGrandTotal()) ]; } /** * @param OrderInterface $order * @return $this */ public function setOrder(OrderInterface $order) { $this->order = $order; return $this; } /** * @return array */ public function customerDepositSync() { $res = []; try { $paymentMethod = $this->order->getPayment()->getMethodInstance(); if (!$paymentMethod->isOffline()) { $postData = $this->customerDepositBuild(); $response = $this->netSuiteAPI->postData(self::NETSUITE_CUSTOMER_DEPOSIT_SERVICE, $postData); if ($response['error'] == true){ $message = sprintf('customer deposit %s sync to Netsuite fail', $this->order->getIncrementId()); $this->logger->debug($message, ['response' => $response, 'postData' => $postData]); } $res['response'] = $response; } else { $res['message'] = 'this order payment method is offline'; } } catch (\Exception $exception) { $res['message'] = $message = sprintf('customer deposit %s sync to Netsuite exception: ', $this->order->getIncrementId(), $exception->getMessage()); $this->logger->debug($message, ['trace' => $exception->getTrace()]); } return $res; } /** * Execute the cron * * @return void */ public function execute(): void { $waitSyncOrders = $this->getWaitSyncOrders()->getItems(); foreach ($waitSyncOrders as $order) { $this->order = $order; /* @var $order \Magento\Sales\Model\Order */ try { $syncOrders = $this->getSyncOrders(); echo '$syncOrders: '.json_encode($syncOrders, JSON_PRETTY_PRINT)."\n"; $index = 1; $orderSyncSuccess = true; foreach ($syncOrders as $sourceCode => $syncOrder) { $locationId = $this->sources[$sourceCode]->getNetsuiteId(); unset($syncOrder['increment_id']); $postData = $this->getPostData($locationId, array_values($syncOrder), $index); $syncOrders[$sourceCode]['increment_id'] = $postData['externalId']; $index++; try { $response = $this->netSuiteAPI->postData(self::NETSUITE_SALES_ORDER_SERVICE, $postData); $this->logger->debug('order sync to netsuite ', ['response' => $response, 'postData' => $postData]); if ($response['error'] != true){ $this->order->setIsNetsuiteSync(1); } else { $orderSyncSuccess = false; $message = sprintf('order %s sync to Netsuite fail', $postData['externalId']); $this->logger->debug($message, ['response' => $response, 'postData' => $postData]); } } catch (InvalidArgumentException $e) { $message = sprintf('order %s sync to Netsuite crash fail: %s', $postData['externalId'], $e->getMessage()); $this->logger->error($message, ['postData' => $postData, 'exception' => $e->getTrace()]); } } $additionalInformation = ['syncOrders' => $syncOrders]; if ($orderSyncSuccess) { $response = $this->customerDepositSync(); $additionalInformation['depositSync'] = $response; } $this->order->setData('additional_information', json_encode($additionalInformation)); $this->orderRepository->save($this->order); } catch (\Exception $exception) { $this->magentoLogger->error('magento sync to netsuite exception: '.$exception->getMessage(), $exception->getTrace()); } } } }