Оглавление
- Создаем модуль «Новая почта» для 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 оформления заказа, по умолчанию выводить только склады в городе пользователя.
Буду рад комментариям, вопросам, предложениям 🙂
ссылка на оригинал статьи http://habrahabr.ru/post/162313/
Добавить комментарий