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 @@
+
+