diff --git a/Console/Command/ExportCommand.php b/Console/Command/ExportCommand.php new file mode 100644 index 0000000..587e3ce --- /dev/null +++ b/Console/Command/ExportCommand.php @@ -0,0 +1,84 @@ +appState = $appState; + $this->queueProcessor = $queueProcessor; + $this->generalHelper = $generalHelper; + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->setName('sales:order:export') + ->setDescription('Sales Order Export'); + } + + /** + * Execute + * @param InputInterface $input + * @param OutputInterface $output + * @return null + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->output = $output; + $this->appState->setAreaCode(\Magento\Framework\App\Area::AREA_GLOBAL); + $this->queueProcessor + ->setTimeInterval(QueueProcessor::LONG_TIME_INTERVAL) + ->process($this); + } + + /** + * Notify + * @param string $message + * @return mixed + */ + public function notify(string $message) + { + $this->output->writeln($message); + } +} \ No newline at end of file diff --git a/Cron/ProcessQueue.php b/Cron/ProcessQueue.php new file mode 100644 index 0000000..8dac46b --- /dev/null +++ b/Cron/ProcessQueue.php @@ -0,0 +1,51 @@ +queueProcessor = $queueProcessor; + $this->generalHelper = $generalHelper; + } + + /** + * Execute + */ + public function execute() + { + if (!$this->generalHelper->isQueueActive()) { + return false; + } + + $this->queueProcessor + ->setTimeInterval(QueueProcessor::DEFAULT_TIME_INTERVAL) + ->process($this); + } + + /** + * Notify + * @param string $message + * @return mixed + */ + public function notify(string $message) + { + // do nothing; + } +} \ No newline at end of file diff --git a/Helper/GeneralHelper.php b/Helper/GeneralHelper.php new file mode 100644 index 0000000..5dbd72f --- /dev/null +++ b/Helper/GeneralHelper.php @@ -0,0 +1,189 @@ +storeManager = $storeManager; + $this->encryptor = $encryptor; + } + + /** + * Check if queue is active + * @return bool + */ + public function isQueueActive() + { + return $this->scopeConfig->isSetFlag(sprintf(self::PATH, 'queue_active')); + } + + /** + * Decrypted config value + * @param $key + * @return string + */ + public function getConfigDecrypted($key) + { + $value = $this->scopeConfig->getValue(sprintf(self::PATH, $key)); + if (empty($value)) { + return ''; + } + + return $this->encryptor->decrypt($value); + } + + /** + * Check if grid active + * @return boolean + */ + public function isAsyncGridActive() + { + return $this->scopeConfig->isSetFlag('dev/grid/async_indexing'); + } + + /** + * Upload directory path + * @return string + */ + public function getUploadDirPath() + { + $path = $this->scopeConfig->getValue(sprintf(self::PATH, 'dropbox_dir_path')); + $path = trim($path); + $path = trim($path, '/'); + + if (empty($path)) { + return '/'; + } + + return '/' . $path . '/'; + } + + /** + * Xml representation of order + * @param \Magento\Sales\Model\Order $order + */ + public function convertToXml(\Magento\Sales\Model\Order $order) + { + $xml = new \DOMDocument('1.0', 'UTF-8'); + $xml->formatOutput = true; + $xml->preserveWhiteSpace = false; + $orderEl = $xml->createElement('order'); + $orderEl->setAttribute('xmlns:xsd', 'http://www.w3.org/2001/XMLSchema'); + $xml->appendChild($orderEl); + + $orderDataEl = $xml->createElement('order_data'); + $orderEl->appendChild($orderDataEl); + $this->addXmlChild($xml, $orderDataEl, 'order_date', $order->getCreatedAt()); + $this->addXmlChild($xml, $orderDataEl, 'item_count', count($order->getAllVisibleItems())); + $this->addXmlChild($xml, $orderDataEl, 'total_item_amount', $order->getBaseSubtotal()); + $this->addXmlChild($xml, $orderDataEl, 'channel', $this->storeManager->getStore($order->getStoreId())->getName()); + $this->addXmlChild($xml, $orderDataEl, 'payment_method', $order->getPayment()->getMethod()); + $this->addXmlChild($xml, $orderDataEl, 'seller_shipping_cost', '0'); + $this->addXmlChild($xml, $orderDataEl, 'reference', $order->getIncrementId()); + $this->addXmlChild($xml, $orderDataEl, 'client_id', $order->getCustomerId() ?: ''); + + $billing = $order->getBillingAddress(); + $billingAddressEl = $xml->createElement('invoice_address'); + $orderEl->appendChild($billingAddressEl); + $this->addXmlChild($xml, $billingAddressEl, 'address_id', $billing->getId()); + $this->addXmlCDataChild($xml, $billingAddressEl, 'firstname', $billing->getFirstname()); + $this->addXmlCDataChild($xml, $billingAddressEl, 'lastname', $billing->getLastname()); + $this->addXmlChild($xml, $billingAddressEl, 'neighbourhood', ''); + $this->addXmlCDataChild($xml, $billingAddressEl, 'street', $billing->getStreetLine(1)); + $this->addXmlCDataChild($xml, $billingAddressEl, 'street_no', $billing->getStreetLine(2)); + $this->addXmlChild($xml, $billingAddressEl, 'zip', $billing->getPostcode()); + $this->addXmlCDataChild($xml, $billingAddressEl, 'city', $billing->getCity()); + $this->addXmlChild($xml, $billingAddressEl, 'country', $billing->getCountryId()); + $this->addXmlChild($xml, $billingAddressEl, 'email', $order->getCustomerEmail()); + $this->addXmlChild($xml, $billingAddressEl, 'phone', $billing->getTelephone()); + $this->addXmlChild($xml, $billingAddressEl, 'rfc', ''); + + $shipping = $order->getShippingAddress(); + $shippingAddressEl = $xml->createElement('shipping_address'); + $orderEl->appendChild($shippingAddressEl); + $this->addXmlChild($xml, $shippingAddressEl, 'address_id', $shipping->getId()); + $this->addXmlCDataChild($xml, $shippingAddressEl, 'firstname', $shipping->getFirstname()); + $this->addXmlCDataChild($xml, $shippingAddressEl, 'lastname', $shipping->getLastname()); + $this->addXmlChild($xml, $shippingAddressEl, 'neighbourhood', ''); + $this->addXmlCDataChild($xml, $shippingAddressEl, 'street', $shipping->getStreetLine(1)); + $this->addXmlCDataChild($xml, $shippingAddressEl, 'street_no', $shipping->getStreetLine(2)); + $this->addXmlChild($xml, $shippingAddressEl, 'zip', $shipping->getPostcode()); + $this->addXmlCDataChild($xml, $shippingAddressEl, 'city', $shipping->getCity()); + $this->addXmlChild($xml, $shippingAddressEl, 'country', $shipping->getCountryId()); + $this->addXmlChild($xml, $shippingAddressEl, 'phone', $shipping->getTelephone()); + + $itemsEl = $xml->createElement('items'); + $xml->appendChild($itemsEl); + + foreach ($order->getAllVisibleItems() as $item) { + /** @var Item $item */ + $itemEl = $xml->createElement('item'); + $itemsEl->appendChild($itemEl); + $this->addXmlChild($xml, $itemEl, 'item_id', $item->getId()); + $this->addXmlChild($xml, $itemEl, 'quantity', $item->getQtyOrdered()); + $this->addXmlCDataChild($xml, $itemEl, 'label', $item->getName()); + $this->addXmlChild($xml, $itemEl, 'item_price', $item->getBasePrice()); + $this->addXmlChild($xml, $itemEl, 'carrier', ''); + $this->addXmlChild($xml, $itemEl, 'tracking_code', ''); + $this->addXmlChild($xml, $itemEl, 'status', $order->getStatus()); + } + + return $xml; + } + + /** + * Append new child with basic value + * @param \DOMDocument $doc + * @param \DOMElement $parentEl + * @param $name + * @param null $value + * @return \DOMElement + */ + protected function addXmlChild(\DOMDocument $doc, \DOMElement $parentEl, $name, $value = null) + { + $el = $doc->createElement($name, $value); + $parentEl->appendChild($el); + + return $el; + } + + /** + * Append new CData section + * @param \DOMDocument $doc + * @param \DOMElement $parentEl + * @param $name + * @param null $value + * @return \DOMElement + */ + protected function addXmlCDataChild(\DOMDocument $doc, \DOMElement $parentEl, $name, $value = null) + { + $el = $doc->createElement($name); + $cdata = $doc->createCDATASection($value); + $el->appendChild($cdata); + $parentEl->appendChild($el); + + return $el; + } +} \ No newline at end of file diff --git a/Model/Api/ApiInterface.php b/Model/Api/ApiInterface.php new file mode 100644 index 0000000..981b838 --- /dev/null +++ b/Model/Api/ApiInterface.php @@ -0,0 +1,8 @@ +generalHelper = $generalHelper; + $this->writeFactory = $writeFactory; + $this->directoryList = $directoryList; + } + + /** + * API + * @return DropboxLib\Dropbox; + */ + protected function getApi() + { + if (!isset($this->api)) { + $key = $this->generalHelper->getConfigDecrypted('dropbox_api_key'); + $secret = $this->generalHelper->getConfigDecrypted('dropbox_api_secret'); + $token = $this->generalHelper->getConfigDecrypted('dropbox_access_token'); + + $dropboxApp = new DropboxLib\DropboxApp($key, $secret, $token); + $this->api = new DropboxLib\Dropbox($dropboxApp); + } + + return $this->api; + } + + /** + * @param Order $order + * @param \DOMDocument $xml + * @return mixed + * @throws \Magento\Framework\Exception\FileSystemException + */ + public function push(Order $order, \DOMDocument $xml) + { + $xml = $xml->saveXML(); + $varDir = $this->writeFactory->create(DirectoryList::VAR_DIR); + + if (!$varDir->isDirectory(self::EXPORT_DIR)) { + $varDir->create(self::EXPORT_DIR); + } + + $exportDir = $this->writeFactory->create(DirectoryList::VAR_DIR . self::EXPORT_DIR); + $fileName = sprintf('%s.xml', $order->getIncrementId()); + $tmpName = md5(microtime(true) . $fileName) . '.xml'; + $exportDir->writeFile($tmpName, $xml); + + $dbFile = $this->getApi()->upload( + $this->directoryList->getRoot() . '/'. $exportDir->getAbsolutePath($tmpName), + $this->generalHelper->getUploadDirPath() . $fileName + ); + + $exportDir->delete($tmpName); + + return $dbFile; + } +} \ No newline at end of file diff --git a/Model/Export.php b/Model/Export.php new file mode 100644 index 0000000..da6fbf1 --- /dev/null +++ b/Model/Export.php @@ -0,0 +1,80 @@ +orderRepository = $orderRepository; + $this->generalHelper = $generalHelper; + } + + /** + * Export + */ + public function _construct() + { + $this->_init(\Julio\Order\Model\ResourceModel\Export::class); + } + + /** + * Is Synced? + * @return bool + */ + public function isSynced() + { + return (bool)(int)$this->_getData('is_synced'); + } + + /** + * return \Magento\Sales\Model\Order + */ + public function getOrder() + { + if (!isset($this->order)) { + $this->order = $this->orderRepository->get($this->getOrderId()); + } + + return $this->order; + } + + /** + * Exports XML from + * @return \DOMDocument + */ + public function asXml() + { + return $this->generalHelper->convertToXml($this->getOrder()); + } +} \ No newline at end of file diff --git a/Model/ExportService.php b/Model/ExportService.php new file mode 100644 index 0000000..0d0511d --- /dev/null +++ b/Model/ExportService.php @@ -0,0 +1,63 @@ +exportFactory = $exportFactory; + $this->exportResource = $exportResource; + } + + /** + * Initialization by orderId + * @param int $orderId + * @return Export + */ + public function initByOrderId(int $orderId): Export + { + /** @var Export $orderExport */ + $orderExport = $this->exportFactory->create(); + $this->exportResource->load($orderExport, $orderId, 'order_id'); + + if ($orderExport->isObjectNew()) { + $orderExport->setData('order_id', $orderId); + } + + return $orderExport; + } + + /** + * Saves Order Export queue item + * @param Export $orderExport + * @return void + * @throws \Magento\Framework\Exception\AlreadyExistsException + */ + public function save(Export $orderExport) + { + $this->exportResource->save($orderExport); + } + + /** + * Clean old + * @return void + */ + public function cleanOld() + { + + } +} \ No newline at end of file diff --git a/Model/ExportServiceInterface.php b/Model/ExportServiceInterface.php new file mode 100644 index 0000000..63aee9a --- /dev/null +++ b/Model/ExportServiceInterface.php @@ -0,0 +1,24 @@ +collectionFactory = $collectionFactory; + $this->logger = $logger; + $this->exportService = $exportService; + $this->api = $api; + $this->grid = $grid; + $this->generalHelper = $generalHelper; + } + + /** + * Interval + * @param $interval + * @return QueueProcessor + */ + public function setTimeInterval($interval) + { + $this->timeInterval = $interval; + + return $this; + } + + /** + * Time interval + * @return int + */ + public function getTimeInterval() + { + return $this->timeInterval; + } + + /** + * Processing the queue + * Number of processed items are dynamically calculated based on TimeInterval. Time interval is period between + * subsequent calls queue processing from Cron. + * @param ProcessObserverInterface $observer + * @return void|boolean + * @throws \Magento\Framework\Exception\AlreadyExistsException + */ + public function process(ProcessObserverInterface $observer) + { + /** @var Collection $queue */ + $queue = $this->collectionFactory + ->create() + ->actAsQueue(); + + if (0 === $queue->count()) { + return false; + } + + $unitTimes = []; + $timeStart = microtime(true); + $timeEnd = $timeStart + $this->getTimeInterval() - self::TIME_MARGIN; + $stillHaveTime = true; + $queueItems = $queue->getIterator(); + $queueItem = current($queueItems); + + do { + try { + $unitTimes[] = $this->processItem($queueItem); + $observer->notify('Processing Item: ' . $queueItem->getId()); + } catch (\Exception $e) { + $this->logger->error(sprintf('XML Order Export: Error during processing Order Queue: %s', $e->getMessage())); + $this->exportService->save($queueItem->setLastError($e->getMessage())); + $observer->notify(sprintf('Processing Item: %d. Error: %s', $queueItem->getId(), $e->getMessage())); + } + $currentTime = microtime(true); + if ($currentTime > $timeEnd - $this->avg($unitTimes)) { + $stillHaveTime = false; + } + $queueItem = next($queueItems); + } while ($queueItem instanceof Export && $stillHaveTime); + } + + /** + * Processing one queue item + * @param Export $queueItem + * @return float + * @throws \Magento\Framework\Exception\AlreadyExistsException + */ + protected function processItem(Export $queueItem) + { + $timeStart = microtime(true); + $this->getApi()->push($queueItem->getOrder(), $queueItem->asXml()); + $queueItem->addData([ + 'last_error' => null, + 'synced' => 1, + 'synced_at' => new \Zend_Db_Expr('NOW()') + ]); + $this->exportService->save($queueItem); + if (!$this->generalHelper->isAsyncGridActive()) { + $this->grid->refresh($queueItem->getOrderId()); + } + $timeFinish = microtime(true); + + return $timeFinish - $timeStart; + } + + /** + * Average time + * @param array $times + * @return float|int + */ + protected function avg(array $times) + { + $count = count($times); + if ($count == 0) { + return 0; + } + + return array_sum($times) / $count; + } + + /** + * Api + * @return ApiInterface + */ + protected function getApi() + { + return $this->api; + } +} \ No newline at end of file diff --git a/Model/ResourceModel/Export.php b/Model/ResourceModel/Export.php new file mode 100644 index 0000000..aaa9b53 --- /dev/null +++ b/Model/ResourceModel/Export.php @@ -0,0 +1,17 @@ +_init(self::TABLE, self::ID_FIELD); + } +} \ No newline at end of file diff --git a/Model/ResourceModel/Export/Collection.php b/Model/ResourceModel/Export/Collection.php new file mode 100644 index 0000000..063503b --- /dev/null +++ b/Model/ResourceModel/Export/Collection.php @@ -0,0 +1,26 @@ +_init(\Julio\Order\Model\Export::class, \Julio\Order\Model\ResourceModel\Export::class); + } + + /** + * Set collection acting as Queue + */ + public function actAsQueue() + { + $this->setOrder('queued_at', \Magento\Framework\Data\Collection::SORT_ORDER_ASC); + $this->addFieldToFilter('synced', 0); + $this->setPageSize(100); + + return $this; + } +} \ No newline at end of file diff --git a/Observer/Order/Export.php b/Observer/Order/Export.php new file mode 100644 index 0000000..bf71a8b --- /dev/null +++ b/Observer/Order/Export.php @@ -0,0 +1,44 @@ +exportService = $exportService; + $this->logger = $logger; + } + + /** + * Collect data about order. + * @param Observer $observer + * @return void + */ + public function execute(Observer $observer) + { + try { + $orderId = $observer->getData('order')->getId(); + $orderExport = $this->exportService->initByOrderId($orderId); + $this->exportService->save($orderExport); + } catch (\Exception $e) { + $this->logger->error(sprintf('Could not create Order Export Queue Entity (%s)', $e->getMessage())); + } + } +} \ No newline at end of file diff --git a/Setup/InstallSchema.php b/Setup/InstallSchema.php new file mode 100644 index 0000000..fcdfea1 --- /dev/null +++ b/Setup/InstallSchema.php @@ -0,0 +1,78 @@ +getConnection(); + $setup->startSetup(); + $table = $connection->newTable($setup->getTable(ExportResource::TABLE)) + ->addColumn( + 'entity_id', + \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, + null, + ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true], + 'Config Id' + )->addColumn( + 'order_id', + \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, + null, + ['unsigned' => true, 'nullable' => false,], + 'Order Reference' + )->addColumn( + 'queued_at', + \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP, + null, + ['nullable' => false, 'default' => \Magento\Framework\DB\Ddl\Table::TIMESTAMP_INIT], + 'Queued At' + )->addColumn( + 'synced_at', + \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP, + null, + ['nullable' => true, 'default' => null], + 'Synced At' + )->addColumn( + 'synced', + \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, + 2, + ['nullable' => false, 'default' => 0], + 'Is Synchronized?' + )->addColumn( + 'last_error', + \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, + '1k', + [], + 'Last Sync Error' + )->addForeignKey( + $connection->getForeignKeyName(ExportResource::TABLE, 'order_id', 'sales_order', 'entity_id'), + 'order_id', + 'sales_order', + 'entity_id', + \Magento\Framework\DB\Ddl\Table::ACTION_CASCADE + )->setComment( + 'Sales Order Export Queue' + ); + $connection->createTable($table); + + $connection->addColumn( + $setup->getTable('sales_order_grid'), + 'export_synced', + [ + 'type' => Table::TYPE_SMALLINT, + 'length' => 2, + 'comment' => 'Export Synced', + 'default' => 0, + 'nullable' => false + ] + ); + + $setup->endSetup(); + } +} diff --git a/Ui/Component/Source/Options/Export.php b/Ui/Component/Source/Options/Export.php new file mode 100644 index 0000000..b415b58 --- /dev/null +++ b/Ui/Component/Source/Options/Export.php @@ -0,0 +1,21 @@ + 0, + 'label' => __('Pending') + ], + [ + 'value' => 1, + 'label' => __('Synced') + ] + ]; + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index a0f6204..f20a826 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "julio.com/order" - ,"version": "0.0.1" + ,"version": "0.0.2" ,"description": "The module exports orders from Magento 2 to Dropbox" ,"type": "magento2-module" ,"homepage": "https://github.com/julio-com/order" @@ -11,7 +11,7 @@ ,"homepage": "https://mage2.pro/users/dmitry_fedyuk" ,"role": "Developer" }] - ,"require": {"julio.com/core": ">=0.0.1", "mage2pro/core": ">=5.1.9"} + ,"require": {"julio.com/core": ">=0.0.1", "kunalvarma05/dropbox-php-sdk": ">=0.2.1", "mage2pro/core": ">=5.1.9"} ,"autoload": {"files": ["registration.php"], "psr-4": {"Julio\\Order\\": ""}} ,"keywords": ["Magento 2"] } \ No newline at end of file diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml new file mode 100644 index 0000000..af82d04 --- /dev/null +++ b/etc/adminhtml/system.xml @@ -0,0 +1,81 @@ + + + +
+ + + + + Magento\Config\Model\Config\Source\Yesno + + + + Magento\Config\Model\Config\Backend\Encrypted + + + + Magento\Config\Model\Config\Backend\Encrypted + + + + Magento\Config\Model\Config\Backend\Encrypted + + + + + +
+
+
diff --git a/etc/config.xml b/etc/config.xml new file mode 100644 index 0000000..95787cd --- /dev/null +++ b/etc/config.xml @@ -0,0 +1,10 @@ + + + + + 0 + Pedidos + + + + \ No newline at end of file diff --git a/etc/crontab.xml b/etc/crontab.xml new file mode 100644 index 0000000..e6f5cc3 --- /dev/null +++ b/etc/crontab.xml @@ -0,0 +1,8 @@ + + + + + */5 * * * * + + + diff --git a/etc/di.xml b/etc/di.xml new file mode 100644 index 0000000..adbdcef --- /dev/null +++ b/etc/di.xml @@ -0,0 +1,34 @@ + + + + + + + Julio\Order\Console\Command\ExportCommand + + + + + + Julio\Order\Model\Api\Dropbox + Magento\Sales\Model\ResourceModel\Order\Grid + + + + + + + sales_order_export_queue + entity_id + order_id + + + + sales_order_export_queue.synced + + + + \ No newline at end of file diff --git a/etc/events.xml b/etc/events.xml new file mode 100644 index 0000000..e11e23a --- /dev/null +++ b/etc/events.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/etc/module.xml b/etc/module.xml index 4a31d36..6bbd08d 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -6,6 +6,7 @@ + \ No newline at end of file diff --git a/view/adminhtml/ui_component/sales_order_grid.xml b/view/adminhtml/ui_component/sales_order_grid.xml new file mode 100644 index 0000000..154c472 --- /dev/null +++ b/view/adminhtml/ui_component/sales_order_grid.xml @@ -0,0 +1,17 @@ + ++ + + + select + + select + + true + + + +