Оглавление
- Создаем модуль «Новая почта» для Magento (часть 1), где мы добавляем новый метод доставки в Magento
- Создаем модуль «Новая почта» для 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 оформления заказа, по умолчанию выводить только склады в городе пользователя.
Буду рад комментариям, вопросам, предложениям :)
Автор: AlexKuk