Pull to refresh

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

Reading time 11 min
Views 23K

Оглавление


  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 оформления заказа, по умолчанию выводить только склады в городе пользователя.

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

P.S. Спасибо юзеру kokoc за полезное замечание. Для работы со структурой таблиц БД, в upgrade скрипте лучше использовать Varien_Db_Ddl_Table. Я поленился и был пойман на горячем :) Запись в виде SQL запроса мне кажется более читабельной, но абстракция важнее.
Tags:
Hubs:
+4
Comments 11
Comments Comments 11

Articles