diff --git a/MoySkladICMLParser.php b/MoySkladICMLParser.php
new file mode 100644
index 0000000..d186d99
--- /dev/null
+++ b/MoySkladICMLParser.php
@@ -0,0 +1,547 @@
+ string
+ *
+ * // директория для размещения итогового ICML (должна существовать)
+ * 'directory' => string
+ *
+ * // не загружать торговые предложения
+ * 'ignoreOffers' => true
+ *
+ * // игнорирование выбранных категорий
+ * 'ignoreCategories' => [
+ * // массив uuid товарных групп, которые будут игнориться
+ * 'uuids' => array,
+ *
+ * // массив внешних кодов товарных групп, которые будут игнориться
+ * 'externalCodes' => array,
+ * ]
+ *
+ * // игнорирование всех категорий
+ * 'ignoreCategories' => 'all'
+ *
+ * 'ignoreProducts' => [
+ * // массив uuid товаров, которые будут игнориться
+ * // (именно товар, модификации игнорить нельзя)
+ * 'uuids' => array,
+ *
+ * // массив внешних кодов товарных групп, которые будут игнориться
+ * // (именно товар, модификации игнорить нельзя)
+ * 'externalCodes' => array,
+ * ]
+ */
+ public function __construct(
+ $login,
+ $pass,
+ $shop,
+ array $options = array()
+ ) {
+ $this->login = $login;
+ $this->pass = $pass;
+ $this->shop = $shop;
+ $this->options = $options;
+ }
+
+ /**
+ * Генерирует ICML файл
+ * @return void
+ */
+ public function generateICML()
+ {
+ $categories = $this->parseProductGroups();
+ $vendors = $this->parseVendors();
+
+ $products = $this->parseProducts($categories, $vendors);
+
+ $icml = $this->createICML($products, $categories);
+
+ $icml->asXML($this->getFilePath());
+ }
+
+ /**
+ * Парсим товарные группы
+ *
+ * @return array
+ */
+ protected function parseProductGroups()
+ {
+ // если парсинг категорий не требуется
+ if (!$this->isProductGroupParseNeed()) {
+ return array();
+ }
+
+ $categories = array();
+
+ $ignoreInfo = $this->getIgnoreProductGroupsInfo();
+ $ignoreUuids = $ignoreInfo[self::UUIDS]; // сюда будут агрегироваться uuid для игнора с учетом вложеностей
+
+ $xml = $this->requestXml(self::GROUP_LIST_URL);
+
+ if ($xml) {
+ foreach ($xml->goodFolder as $goodFolder) {
+ $uuid = (string) $goodFolder->uuid;
+ $externalCode = (string) $goodFolder->externalcode;
+ $parentUuid = isset($goodFolder[0]['parentUuid']) ?
+ (string) $goodFolder[0]['parentUuid'] : null;
+
+ // смотрим игноры
+ if (in_array($uuid, $ignoreInfo[self::UUIDS])) {
+ continue;
+ } elseif (in_array($externalCode, $ignoreInfo[self::EXTERNAL_CODES])) {
+ $ignoreUuids[] = $uuid;
+ continue;
+ } elseif (
+ $parentUuid
+ && in_array($parentUuid, $ignoreUuids)
+ ) {
+ $ignoreUuids[] = $uuid;
+ continue;
+ }
+
+ $category = array(
+ 'uuid' => $uuid,
+ 'name' => (string) $goodFolder[0]['name'],
+ 'externalCode' => $externalCode,
+ );
+
+ if (isset($goodFolder[0]['parentUuid'])) {
+ $category['parentUuid'] = (string) $goodFolder[0]['parentUuid'];
+ }
+
+ $categories[$uuid] = $category;
+ }
+ }
+
+ $result = array();
+ $this->sortGroupTree($result, $categories);
+
+ return $result;
+ }
+
+ /**
+ * Парсим производителей
+ *
+ * @return array
+ */
+ protected function parseVendors()
+ {
+ $vendors = array();
+
+ $xml = $this->requestXml(self::COMPANY_LIST_URL);
+
+ if ($xml) {
+ foreach ($xml->company as $c) {
+ $uuid = (string) $c->uuid;
+ $name = (string) $c[0]['name'];
+ $vendors[$uuid] = $name;
+ }
+ }
+
+ return $vendors;
+ }
+
+ /**
+ * Парсим товары
+ *
+ * @param array $categories
+ * @param array $vendors
+ * @return array
+ */
+ protected function parseProducts(
+ $categories = array(),
+ $vendors = array()
+ ) {
+ $products = array();
+
+ $xml = $this->requestXml(self::PRODUCT_LIST_URL);
+ if ($xml) {
+ foreach ($xml->good as $v) {
+
+ $parentUuid = isset($v[0]['parentUuid']) ?
+ (string) $v[0]['parentUuid'] : null;
+ $categoryId = $parentUuid && isset($categories[$parentUuid]) ?
+ $categories[$parentUuid]['externalCode'] : '';
+ $vendorUuid = isset($v[0]['supplierUuid']) ?
+ (string) $v[0]['supplierUuid'] : null;
+
+ $uuid = (string) $v->uuid;
+ $exCode = (string) $v->externalcode;
+ $products[$uuid] = array(
+ 'id' => $exCode, // тут либо externalcode либо uuid товара
+ 'exCode' => $exCode, // сюда пишем externalcode
+ 'name' => (string) $v[0]['name'],
+ 'price' => ((int) $v[0]['salePrice']) / 100,
+ 'purchasePrice' => ((int) $v[0]['buyPrice']) / 100,
+ 'article' => (string) $v[0]['productCode'],
+ 'vendor' => $vendorUuid && isset($vendors[$vendorUuid]) ?
+ $vendors[$vendorUuid] : '',
+ 'categoryId' => $categoryId,
+ 'offers' => array(),
+ );
+ }
+ }
+
+ if (!$this->isIgnoreOffers()) {
+ $xml = $this->requestXml(self::OFFER_LIST_URL);
+ if ($xml) {
+ foreach ($xml->consignment as $c) {
+ // если нет feature, то товар без торговых предложений
+ if (!isset($c->feature)) {
+ continue;
+ }
+
+ $exCode = (string)$c->feature->externalcode;
+ $name = (string)$c[0]['name'];
+ $pid = (string)$c[0]['goodUuid'];
+
+ if (isset($products[$pid])) {
+ $products[$pid]['offers'][$exCode] = array(
+ 'id' => $products[$pid]['exCode'] . '#' . $exCode,
+ 'name' => $name,
+ );
+ } else {
+ // иначе это не товар а услуга (service)
+ }
+ }
+ }
+ }
+
+ // для товаров без торговых преложений
+ foreach ($products as $key1 => &$product) {
+ // если нет торговых предложений
+ if (empty($product['offers'])) {
+ $product['offers'][] = array(
+ 'id' => $product['exCode'],
+ 'name' => $product['name'],
+ );
+ }
+ }
+
+ return $products;
+ }
+
+ /**
+ * Требуется ли загрузка категорий
+ */
+ protected function isProductGroupParseNeed()
+ {
+ if (isset($this->options[self::OPTION_IGNORE_CATEGORIES])) {
+ $ignore = $this->options[self::OPTION_IGNORE_CATEGORIES];
+ if ($ignore === self::IGNORE_ALL_CATEGORIES) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Получаем данные для игнорирования товарных групп
+ */
+ protected function getIgnoreProductGroupsInfo()
+ {
+ if (
+ !isset($this->options[self::OPTION_IGNORE_CATEGORIES])
+ || !is_array($this->options[self::OPTION_IGNORE_CATEGORIES])
+ ) {
+ $info = array();
+ } else {
+ $info = $this->options[self::OPTION_IGNORE_CATEGORIES];
+ }
+
+ if (
+ !isset($info[self::UUIDS])
+ || !is_array($info[self::UUIDS])
+ ) {
+ $info[self::UUIDS] = array();
+ }
+ if (
+ !isset($info[self::EXTERNAL_CODES])
+ || !is_array($info[self::EXTERNAL_CODES])
+ ) {
+ $info[self::EXTERNAL_CODES] = array();
+ }
+
+ return $info;
+ }
+
+ /**
+ * @param string $uri
+ * @return SimpleXMLElement
+ */
+ protected function requestXml($uri)
+ {
+ $url = self::BASE_URL . $uri;
+
+ $ch = curl_init();
+
+ $securityStr = base64_encode($this->login.':'.$this->pass);
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt(
+ $ch,
+ CURLOPT_HTTPHEADER,
+ array(
+ "Authorization: Basic ".$securityStr."\r\n", // авторизация
+ )
+ );
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // возвращаем результат
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::TIMEOUT);
+
+ $result = curl_exec($ch);
+ curl_close($ch);
+
+ if ($result === false) {
+ return null;
+ }
+
+ try {
+ $xml = new SimpleXMLElement($result);
+ } catch (Exception $e) {
+ return null;
+ }
+
+ return $xml;
+ }
+
+ /**
+ * Сортируем массив согласно parentId (родитель идет до потомков)
+ *
+ * @param array &$result
+ * @param array $arr
+ * @param array $prev
+ * @return void
+ */
+ protected function sortGroupTree(&$result, $arr, $prev = array())
+ {
+ if (empty($arr)) {
+ return;
+ }
+
+ $checkPrev = function (&$result, &$prev) {
+ foreach ($prev as $key => $elem) {
+ if (isset($result[$elem['parentUuid']])) {
+ $result[$elem['uuid']] = $elem;
+ unset($prev[$key]);
+ }
+ }
+ };
+
+ $elem = array_shift($arr);
+ if (isset($elem['parentUuid'])) {
+ if (isset($result[$elem['parentUuid']])) {
+ $result[$elem['uuid']] = $elem;
+ $this->sortGroupTree($result, $arr, $prev);
+
+ $checkPrev($result, $prev);
+ } else {
+ $prev[] = $elem;
+ $this->sortGroupTree($result, $arr, $prev);
+ }
+ } else {
+ $result[$elem['uuid']] = $elem;
+
+ $checkPrev($result, $prev);
+
+ $this->sortGroupTree($result, $arr, $prev);
+ }
+ }
+
+ /**
+ * Игнорировать торговые предложения
+ */
+ protected function isIgnoreOffers()
+ {
+ if (
+ isset($this->options['ignoreOffers'])
+ && true === $this->options['ignoreOffers']
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Формируем итоговый ICML
+ *
+ * @param array $products
+ * @param array $categories
+ * @return SimpleXMLElement
+ */
+ protected function createICML(
+ $products,
+ $categories
+ ) {
+ $date = new DateTime();
+ $xmlstr = ''.$this->shop.'';
+ $xml = new SimpleXMLElement($xmlstr);
+
+ if (count($categories)) {
+ $categoriesXml = $this->icmlAdd($xml->shop, 'categories', '');
+ foreach ($categories as $category) {
+ $categoryXml = $this->icmlAdd($categoriesXml, 'category', $category['name']);
+ $categoryXml->addAttribute('id', $category['externalCode']);
+
+ if (isset($category['parentUuid']) && $category['parentUuid']) {
+ $parentUuid = $category['parentUuid'];
+
+ if (isset($categories[$parentUuid])) {
+ $categoryXml->addAttribute('parentId', $categories[$parentUuid]['externalCode']);
+ } else {
+ throw new RuntimeException('Can\'t find category with uuid = \''.$parentUuid.'\'');
+ }
+ }
+ }
+ }
+
+ $offersXml = $this->icmlAdd($xml->shop, 'offers', '');;
+ foreach ($products as $key1 => $product) {
+ foreach ($product['offers'] as $key2 => $offer) {
+ $offerXml = $offersXml->addChild('offer');
+ $offerXml->addAttribute('id', $offer['id']);
+ $offerXml->addAttribute('productId', $product['id']);
+
+ $this->icmlAdd($offerXml, 'xmlId', $offer['id']);
+ $this->icmlAdd($offerXml, 'price', number_format($product['price'], 2, '.', ''));
+ $this->icmlAdd($offerXml, 'purchasePrice', number_format($product['purchasePrice'], 2, '.', ''));
+ $this->icmlAdd($offerXml, 'name', $offer['name']);
+ $this->icmlAdd($offerXml, 'productName', $product['name']);
+
+ if ($product['categoryId']) {
+ $this->icmlAdd($offerXml, 'categoryId', $product['categoryId']);
+ }
+
+ if ($product['article']) {
+ $art = $this->icmlAdd($offerXml, 'param', $product['article']);
+ $art->addAttribute('name', 'article');
+ }
+
+ if ($product['vendor']) {
+ $this->icmlAdd($offerXml, 'vendor', $product['vendor']);
+ }
+ }
+ }
+
+ return $xml;
+ }
+
+ /**
+ * Добавляем элемент в icml
+ *
+ * @param SimpleXMLElement $xml
+ * @param string $name
+ * @param string $value
+ * @return SimpleXMLElement
+ */
+ protected function icmlAdd(
+ SimpleXMLElement $xml,
+ $name,
+ $value
+ ) {
+ $elem = $xml->addChild($name);
+ if ($value !== '') {
+ $elem->{0} = $value;
+ }
+
+ return $elem;
+ }
+
+ /**
+ * Возвращает имя ICML-файла
+ * @return string
+ */
+ protected function getFilePath()
+ {
+ $path = isset($this->options['directory']) && $this->options['directory'] ?
+ $this->options['directory'] : __DIR__;
+
+ if (substr($path, -1) === '/') {
+ $path = substr($path, 0, -1);
+ }
+
+ $file = isset($this->options['file']) && $this->options['file'] ?
+ $this->options['file'] : $this->shop.'.catalog.xml';
+
+ return $path.'/'.$file;
+ }
+}
diff --git a/README.md b/README.md
index 9af9703..be1e395 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,38 @@
-moyskad-catalog
-===============
+# moyskad-catalog
ICML generator for the MoySklad catalog
+
+## Usage
+
+1) Include file 'MoySkladICMLParser.php'
+
+2) Configure parser
+
+```php
+$parser = new MoySkladICMLParser(
+ 'login@moysklad',
+ 'password',
+ 'shopname',
+ $options
+);
+```
+
+3) Call `generateICML` method
+
+See file `example.php` for simple usage example.
+
+## Options
+
+Options is array with next keys:
+
+* `file` - filename with result icml without path (default: .catalog.xml)
+* `directory` - target directory for icml file (default: current directory)
+* `ignoreOffers` - if `true` consignment from MoySklad will be ignored
+* `ignoreCategories` - string `'all'` or array with keys:
+ * `uuids` - array with GoodFolder `uuid` for ignore
+ * `externalCodes` - array with GoodFolder `externalcode` for ignore
+* `ignoreProducts` - array with keys:
+ * `uuids` - array with Good `uuid` for ignore (Consignment can't be ignore)
+ * `externalCodes` - array with Good `externalcode` for ignore (Consignment can't be ignore)
+
+All options keys aren't required
\ No newline at end of file
diff --git a/example.php b/example.php
new file mode 100644
index 0000000..3286752
--- /dev/null
+++ b/example.php
@@ -0,0 +1,17 @@
+ __DIR__,
+ 'file' => 'test.xml',
+ )
+);
+
+// generate
+$parser->generateICML();
\ No newline at end of file