Создаем модуль «Новая почта» для Magento (часть 2)

от автора

Оглавление

  1. Создаем модуль «Новая почта» для Magento (часть 1), где мы добавляем новый метод доставки в Magento
  2. Создаем модуль «Новая почта» для Magento (часть 2), где мы учим Magento хранить и синхронизировать с Новой Почтой базу складов

После перерыва, связанного с запуском проекта для вредного заказчика, я продолжу начатое. Напомню, все исходники можно найти на GitHub: github.com/alexkuk/Ak_NovaPoshta/, они дополняются по ходу разработки.

В этой части мы получим API ключ и напишем синхронизацию складов и городов из Новой Почты в базу Magento.

В итоге мы получим такую таблицу в панели администратора:

API Новой Почты

Создается такое впечатление, что Новая Почта скрывает свой API как только может. Даже о его существовании я узнал со сторонних форумов.

Первое, что нужно сделать для получения доступа — зарегистрироваться в программе лояльности в отделении Новой Почты. В итоге вы получите логин и пароль для доступа к своему личному кабинету. На странице этого кабинета также нет упоминаний об API, но добрые люди в интернетах указывают на следующий адрес: orders.novaposhta.ua/api.php?todo=api_form.

Ура! У нас есть документация и даже форма для тестирования запросов. Но нужен еще и ключ. Здесь снова понадобилась помощь добрых людей — для того, чтобы увидеть свой ключ, нужно перейти по этому адресу: orders.novaposhta.ua/api.php?todo=api_get_key_ajax.

Доступ к API есть, вернемся к Magento.

Добавим конфигурационные опции

Сделаем конфигурируемым URL и ключ API. Также при работе с API будет полезным писать свой отключаемый лог.

В system.xml в добавим следующие поля:

                       <api_url translate="label">                            <label>API URL</label>                            <frontend_type>text</frontend_type>                            <sort_order>120</sort_order>                            <show_in_default>1</show_in_default>                            <show_in_website>0</show_in_website>                            <show_in_store>0</show_in_store>                        </api_url>                        <api_key translate="label">                            <label>API key</label>                            <frontend_type>text</frontend_type>                            <sort_order>130</sort_order>                            <show_in_default>1</show_in_default>                            <show_in_website>0</show_in_website>                            <show_in_store>0</show_in_store>                        </api_key>                        <enable_log translate="label">                            <label>Enable log</label>                            <frontend_type>select</frontend_type>                            <source_model>adminhtml/system_config_source_yesno</source_model>                            <sort_order>140</sort_order>                            <show_in_default>1</show_in_default>                            <show_in_website>0</show_in_website>                            <show_in_store>0</show_in_store>                        </enable_log> 

В config.xml добавим значения по умолчанию:

<config>     ...      <default>            <carriers>                    <novaposhta>                        ...                        <api_url>http://orders.novaposhta.ua/xml.php</api_url>                        <enable_log>0</enable_log>                    </novaposhta>            </carriers>     </default>     ... </config> 

В хелпере реализуем метод доступа к значениям конфигурации и метод записи в лог. Такие мелкие вещи, используемые в разных частях модуля, удобно вынести в хелпер. Нужно также понимать, что Mage::helper(‘novaposhta’) возвращает синглтон нашего хелпера.

class Ak_NovaPoshta_Helper_Data extends Mage_Core_Helper_Abstract {     protected $_logFile = 'novaposhta.log';      /**     * @param $string     *     * @return Ak_NovaPoshta_Helper_Data     */     public function log($string)     {            if ($this->getStoreConfig('enable_log')) {                    Mage::log($string, null, $this->_logFile);            }            return $this;     }      /**     * @param string $key     * @param null $storeId     *     * @return mixed     */     public function getStoreConfig($key, $storeId = null)     {            return Mage::getStoreConfig("carriers/novaposhta/$key", $storeId);     } } 

Готовим БД

Добавим свои таблицы в базу данных. Для этого используем встроенный в Magento механизм обновлений (подробнее можете почитать в этой статье codemagento.com/2011/02/altering-the-database-through-setup-scripts/).

Сперва опишем добавляемые ресурсы и сущности, а также добавим ресурс novaposhta_setup в config.xml:

... <global>        <models>                <novaposhta>                    <class>Ak_NovaPoshta_Model</class>                    <resourceModel>novaposhta_resource</resourceModel>                </novaposhta>                <novaposhta_resource>                    <class>Ak_NovaPoshta_Model_Resource</class>                    <entities>                            <city>                                    <table>novaposhta_city</table>                            </city>                            <warehouse>                                    <table>novaposhta_warehouse</table>                            </warehouse>                    </entities>                <novaposhta_resource>        </models>        ...        <resources>                <novaposhta_setup>                    <setup>                            <module>Ak_NovaPoshta</module>                    </setup>                </novaposhta_setup>        </resources> </global> ... 

Добавим upgrade скрипт app/code/community/Ak/NovaPoshta/sql/novaposhta_setup/mysql4-upgrade-1.0.0-1.0.1.php, в котором создадим необходимые нам таблицы.

/* @var $installer Mage_Core_Model_Resource_Setup */ $installer = $this;  $installer->startSetup();  $installer->run(" CREATE TABLE {$this->getTable('novaposhta_city')} (  `id` int(10) unsigned NOT NULL,  `name_ru` varchar(100),  `name_ua` varchar(100),  `updated_at` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,  PRIMARY KEY (`id`),  INDEX `name_ru` (`name_ru`),  INDEX `name_ua` (`name_ua`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;  CREATE TABLE {$this->getTable('novaposhta_warehouse')} (  `id` int(10) unsigned NOT NULL,  `city_id` int(10) unsigned NOT NULL,  `address_ru` varchar(200),  `address_ua` varchar(200),  `phone` varchar(100),  `weekday_work_hours` varchar(20),  `weekday_reseiving_hours` varchar(20),  `weekday_delivery_hours` varchar(20),  `saturday_work_hours` varchar(20),  `saturday_reseiving_hours` varchar(20),  `saturday_delivery_hours` varchar(20),  `max_weight_allowed` int(4),  `longitude` float(10,6),  `latitude` float(10,6),  `number_in_city` int(3) unsigned NOT NULL,  `updated_at` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,  PRIMARY KEY (`id`),  CONSTRAINT FOREIGN KEY (`city_id`) REFERENCES `{$this->getTable('novaposhta_city')}` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ");  $installer->endSetup(); 

Осталось поднять версию модуля до 1.0.1 в нашем config.xml, очистить кеш, запустить Magento и можно проверять, создались ли таблицы в базе. Создались, идем дальше.

Создадим модели, ресурсы и коллекции

Мы добавляем сущности city и warehouse. Для того, чтобы работать с ними, нам необходимо создать соответствующие модели Ak_NovaPoshta_Model_City и Ak_NovaPoshta_Model_Warehouse. Для того, чтобы сохранять их в базе создадим ресурсы Ak_NovaPoshta_Model_Resource_City и Ak_NovaPoshta_Model_Resource_Warehouse. Для связи модели с ресурсом в классе модели в псевдоконструкторе вызовем метод _init() c алиасом класса ресурса в качестве параметра:

class Ak_NovaPoshta_Model_City extends Mage_Core_Model_Abstract {     public function _construct()     {            $this->_init('novaposhta/city');     } … } 

В ресурсе вызовем _init() ресурса, в который передадим алиас таблицы БД и имя primary key поля.

class Ak_NovaPoshta_Model_Resource_City extends Mage_Core_Model_Resource_Db_Abstract {     public function _construct()     {        $this->_init('novaposhta/city', 'id');     } } 

Также добавим коллекции Ak_NovaPoshta_Model_Resource_City_Collection и Ak_NovaPoshta_Model_Resource_Warehouse_Collection. В вызов метода _init() передаем алиас модели. Пример Ak_NovaPoshta_Model_Resource_City_Collection:

class Ak_NovaPoshta_Model_Resource_City_Collection extends Mage_Core_Model_Resource_Db_Collection_Abstract {     public function _construct()     {        $this->_init('novaposhta/city');     } } 

Модель клиента API

Создадим модель Ak_NovaPoshta_Model_Api_Client, которая будет скрывать логику работы с API. Код клиента: github.com/alexkuk/Ak_NovaPoshta/blob/master/app/code/community/Ak/NovaPoshta/Model/Api/Client.php
Наш новоиспеченный клиент имеет два публичных метода: getCityWarehouses() возвращает города, в которых есть представительства Новой Почты, getWarehouses() возвращает список складов по всей Украине. Данные возвращаются в виде SimpleXMLElement объекта.

Импорт

Добавим модель Ak_NovaPoshta_Model_Import: github.com/alexkuk/Ak_NovaPoshta/blob/master/app/code/community/Ak/NovaPoshta/Model/Import.php. Описывать подробно процесс импорта смысла нет. Остановлюсь лишь на некоторых вещах.

Я добавил два массива $_dataMapCity и $_dataMapWarehouse, которые связывают именя полей, возвращаемых API с именами поле в нашей базе. После получения ответа от API приводим ответ к нужному нам виду с помощью метода _applyMap():

$cities = $this->_applyMap($cities, $this->_dataMapCity); 

Для того, чтобы записывать данные в БД при иморте, я не использую модели City и Warehouse, а напрямую выполняю SQL запрос, предварительно разбив его на части. Запрос выполняю с помощью core_write ресурса:

  /**     * @return Varien_Db_Adapter_Interface     */     protected function _getConnection()     {            return Mage::getSingleton('core/resource')->getConnection('core_write');     } 

Для тестирования модели Import я бросил скрипт test.php в корень Magento. В нем инициализируем Magento вызовом метода Mage::app(), после чего можно пользоваться фабрикой Mage:

require 'app/Mage.php'; Mage::app('default'); Mage::getModel('novaposhta/import')->runWarehouseAndCityMassImport(); 

Запуск импорта по CRONу

Импорт готов и отлажен, хорошо бы теперь запускать его периодически по CRONу. В Magento есть своя CRON подсистема. Почитать можно, например, тут: www.magentocommerce.com/wiki/1_-_installation_and_configuration/how_to_setup_a_cron_job. В двух словах: в привычный Unix cron добавляем cron job, который будет запускать cron.php или cron.sh скрипт, который в свою очередь запускает подсистему CRON Magento. В рамках этого вызова и выполняются все задачи, добавленные модулями через config.xml.

Итак, добавим нашу задачу в config.xml:

    <crontab>            <jobs>                    <novaposhta_import_city_and_warehouse>                        <schedule>                                <cron_expr>1 2 * * *</cron_expr>                        </schedule>                        <run>                         <model>ak_novaposhta/import::runWarehouseAndCityMassImport</model>                        </run>                    </novaposhta_import_city_and_warehouse>            </jobs>     </crontab> 

Добавим таблицу складов в панель администратора

Для создания грида как на картинке выше нам необходимо два класса блока: класс контейнера грида и класс самого грида. Контейнер, унаследованный от Mage_Adminhtml_Block_Widget_Grid_Container, определяет внешний вид и поведение кнопок, а также выводит сам грид Mage_Adminhtml_Block_Widget_Grid.

Ах да, еще понадобится контроллер 🙂

Итак, Ak_NovaPoshta_Block_Adminhtml_Warehouses:

class Ak_NovaPoshta_Block_Adminhtml_Warehouses extends Mage_Adminhtml_Block_Widget_Grid_Container {     public function __construct()     {            // $this->_blockGroup и $this->_controller нужны для того, чтобы родительский _prepareLayout() нашел правильный класс грида (novaposhta/adminhtml_warehouses). В качестве альтернативы можно переписать _prepareLayout().            $this->_blockGroup = 'novaposhta';            $this->_controller = 'adminhtml_warehouses';            this->_headerText = $this->__('Manage warehouses');             parent::__construct();            // удаляем кнопку add, добавленную в родительском конструкторе, мы не хотим позволять добавлять склады из админки            $this->_removeButton('add');            // добавляем свою кнопку, которая будет запускать синхронизацию            $this->_addButton('synchronize', array(                    'label'     => $this->__('Synchronize with API'),                    'onclick'   => 'setLocation(\'' . $this->getUrl('*/*/synchronize') .'\')'            ));     } } 

Класс грида:

class Ak_NovaPoshta_Block_Adminhtml_Warehouses_Grid extends Mage_Adminhtml_Block_Widget_Grid {     public function __construct()     {            parent::__construct();            $this->setDefaultSort('city_id');            $this->setId('warehousesGrid');            $this->setDefaultDir('asc');            $this->setSaveParametersInSession(true);     }      protected function _prepareCollection()     {            /** @var $collection Ak_NovaPoshta_Model_Resource_Warehouse_Collection */            $collection = Mage::getModel('novaposhta/warehouse')                    ->getCollection();             $this->setCollection($collection);            return parent::_prepareCollection();     }      protected function _prepareColumns()     {            // Описываем колонки грида            $this->addColumn('id',                    array(                        'header' => $this->__('ID'),                        'align' =>'right',                        'width' => '50px',                        'index' => 'id'                    )            );             $this->addColumn('address_ru',                    array(                        'header' => $this->__('Address (ru)'),                        'index' => 'address_ru'                    )            );             $this->addColumn('city_id',                    array(                         'header' => $this->__('City'),                         'index' => 'city_id',                         'type'  => 'options',                         // В качестве опций для колонки City используем массив названий городов вместо “сухих” идентификаторов                         'options' => Mage::getModel('novaposhta/city')->getOptionArray()                    )            );             $this->addColumn('phone',                    array(                         'header' => $this->__('Phone'),                         'index' => 'phone'                    )            );             $this->addColumn('max_weight_allowed',                    array(                         'header' => $this->__('Max weight'),                         'index' => 'max_weight_allowed'                    )            );             return parent::_prepareColumns();     }      // возвращаем false - не хотим давать возможность переходить на редактирование строки     public function getRowUrl($row)     {            return false;     }  } 

Теперь контроллер. Так как контролле для админки, наследуемся от Mage_Adminhtml_Controller_Action.

class Ak_NovaPoshta_WarehousesController extends Mage_Adminhtml_Controller_Action {     /**     * здесь создаем блок контейнера грида и рендерим     * /     public function indexAction()     {            $this->_title($this->__('Sales'))->_title($this->__('Nova Poshta Warehouses'));             $this->_initAction()            ->_addContent($this->getLayout()->createBlock('novaposhta/adminhtml_warehouses'))            ->renderLayout();         return $this;     }      /**     * здесь запускаем синхронизацию     * /     public function synchronizeAction()     {        try {            Mage::getModel('novaposhta/import')->runWarehouseAndCityMassImport();            // Успех, добавляем success message в стек уведомлений            $this->_getSession()->addSuccess($this->__('City and Warehouse API synchronization finished'));        }        catch (Exception $e) {              // Исключение, добавляем error message в стек уведомлений            $this->_getSession()->addError($this->__('Error during synchronization: %s', $e->getMessage()));        }         // возвращаемся на страницу с контейнером грида        $this->_redirect('*/*/index');         return $this;     }      /**     * Initialize action     *     * @return Ak_NovaPoshta_WarehousesController     */     protected function _initAction()     {        $this->loadLayout()            ->_setActiveMenu('sales/novaposhta/warehouses')            ->_addBreadcrumb($this->__('Sales'), $this->__('Sales'))            ->_addBreadcrumb($this->__('Nova Poshta Warehouses'), $this->__('Nova Poshta Warehouses'))        ;        return $this;     } } 

Но это еще не все. Во-первых, нам нужно добавить роут в config.xml, чтобы Magento смогла найти наш контроллер.

<config> ...     <admin>        <routers>            <novaposhta>                <use>admin</use>                <args>                    <module>Ak_NovaPoshta</module>                    <frontName>novaposhta</frontName>                </args>            </novaposhta>        </routers>     </admin> ... </config> 

Во-вторых, нам нужно добавить пункт в меню администратора и добавить его в ACL. Все это вписываем в adminhtml.xml:

<?xml version="1.0"?> <config>     <menu>         <sales>            <children>                <novaposhta translate="title" module="novaposhta">                    <sort_order>200</sort_order>                    <title>Nova Poshta</title>                    <children>                        <warehouses translate="title" module="novaposhta">                            <sort_order>10</sort_order>                            <title>Warehouses</title>                            <action>novaposhta/warehouses/</action>                        </warehouses>                    </children>                </novaposhta>            </children>         </sales>     </menu>     <acl>        <resources>            <admin>                <children>                    <sales>                        <children>                            <novaposhta translate="title" module="novaposhta">                                <title>Nova Poshta</title>                                <sort_order>200</sort_order>                                <children>                                    <warehouses translate="title" module="novaposhta">                                        <sort_order>10</sort_order>                                        <title>Warehouses</title>                                    </warehouses>                                </children>                            </novaposhta>                        </children>                    </sales>                </children>            </admin>        </resources>     </acl> </config> 

Готово

У нас работает синхронизация и есть достаточно удобный интерфейс для просмотра складов. Следующая задача — выводить склады Новой Почты в удобном для выбора виде на шаге Shipping Method оформления заказа, по умолчанию выводить только склады в городе пользователя.

Буду рад комментариям, вопросам, предложениям 🙂

ссылка на оригинал статьи http://habrahabr.ru/post/162313/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *