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

в 8:47, , рубрики: ecommerce, Magento, php, новая почта, электронная коммерция, метки: , , , ,

Оглавление

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

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

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

В итоге мы получим такую таблицу в панели администратора:
Создаем модуль «Новая почта» для Magento (часть 2)

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

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

Автор: AlexKuk

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js