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(); parent::__construct(); } protected function configure() { $this->setName('order:netsuite:sync'); $this->setDescription('Sync order to netsuite manually, separate multiple order id with comma.'); $this->addOption( 'ordersId', 'ordersId', InputOption::VALUE_REQUIRED, ",\r\n ordersId is required\r\n" ); } /** * @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) && !empty($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->getAllItems() as $item) { /* @var $item OrderItemInterface */ if ($item->getProductType()===Configurable::TYPE_CODE){ continue; } $inventorySourceFromProduct = $this->getInventorySourceFromProduct($item); foreach ($inventorySourceFromProduct 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->getEntityId().'-'.$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() { $ordersId = explode(',', $this->input->getOption('ordersId')); $this->output->writeln('received orders id: '.implode(',', $ordersId)); $criteria = $this->searchCriteriaBuilderFactory->create() ->addFilter('is_netsuite_sync', 0) ->addFilter('entity_id', $ordersId, 'in') ->create(); return $this->orderRepository->getList($criteria); } protected function customerDepositBuild() { $dataCustomer = $this->getCustomer(); return [ 'externalId'=> 'CD-'.$this->order->getEntityId().'-1', 'customer' => ['id' => $dataCustomer->getCustomAttribute('netsuite_id')?->getValue()], 'salesOrder' => ['externalId' => 'Magento-'.$this->order->getEntityId().'-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->output->writeln($message); $this->output->writeln(sprintf('customer deposit %s sync response: %s', $this->order->getIncrementId(), json_encode($response))); $this->output->writeln(sprintf('customer deposit %s sync postData: %s', $this->order->getIncrementId(), json_encode($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->output->writeln($message); } return $res; } /** * look for netsuite order id from http response header Location * @param array $responseHeaders * @return string */ protected function getNetsuiteOrderId(array $responseHeaders) { if (!empty($responseHeaders['Location'])) { $isMatch = preg_match('/salesorder\/(\d+)/', $responseHeaders['Location'], $matches); if ($isMatch) { return $matches[1]; } } return ''; } /** * */ public function execute(InputInterface $input, OutputInterface $output): int { $this->input = $input; $this->output = $output; // $waitSyncOrders = $this->getWaitSyncOrders()->getItems(); // // foreach ($waitSyncOrders as $order) { // $this->order = $order; // /* @var $order \Magento\Sales\Model\Order */ // try { // $syncOrders = $this->getSyncOrders(); // // $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); // // if ($response['error'] != true){ // $syncOrders[$sourceCode]['netsuite_order_id'] = $this->getNetsuiteOrderId($this->netSuiteAPI->getResponseHeaders()); // $this->order->setIsNetsuiteSync(1); // } else { // $orderSyncSuccess = false; // $message = sprintf('order %s sync to Netsuite fail', $postData['externalId']); // $this->output->writeln($message); // $this->output->writeln(sprintf('customer deposit %s sync response: %s', $this->order->getIncrementId(), json_encode($response))); // $this->output->writeln(sprintf('customer deposit %s sync postData: %s', $this->order->getIncrementId(), json_encode($postData))); // } // } catch (InvalidArgumentException $e) { // $message = sprintf('order %s sync to Netsuite crash fail: %s', $postData['externalId'], $e->getMessage()); // $this->output->writeln($message); // } // // } // $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->output->writeln('magento sync to netsuite exception: '.$exception->getMessage()); // $this->output->writeln('sync exception: '.json_encode($exception->getTrace())); // } // } $product = $this->productRepository->getById(3657); var_dump($product->getExtensionAttributes()->getNetsuiteId(), $product->getNetsuiteId()); return Command::SUCCESS; } }