Commit eaa0aec9 authored by Batyr Mackenov's avatar Batyr Mackenov

Multiple currencies support

parent f71d1ab6
<?php
//##copyright##
class iaBackendController extends iaAbstractControllerModuleBackend
{
protected $_name = 'currencies';
protected $_table = 'currencies';
protected $_gridColumns = '`code`, `title`, `rate`, `default`, `order`, `code` `id`, 1 `update`, 1 `delete`';
protected $_gridFilters = ['code' => self::EQUAL, 'title' => self::LIKE];
public function init()
{
$this->_path = IA_ADMIN_URL . $this->getModuleName() . IA_URL_DELIMITER . $this->getName() . IA_URL_DELIMITER;
$this->_template = 'form-currencies';
}
protected function _setPageTitle(&$iaView, array $entryData, $action)
{
$iaView->title(iaLanguage::getf($action . '_currency', $entryData));
}
public function getById($id)
{
return $this->_iaDb->row(iaDb::ALL_COLUMNS_SELECTION, iaDb::convertIds($id, 'code'));
}
protected function _entryUpdate(array $entryData, $entryId)
{
$this->_iaDb->update($entryData, iaDb::convertIds($entryId, 'code'));
return (0 === $this->_iaDb->getErrorNumber());
}
protected function _setDefaultValues(array &$entry)
{
$entry = [
'code' => '',
'title' => '',
'rate' => 0.00,
'sym' => '',
'sym_pos' => 'pre',
'default' => false
];
}
protected function _preSaveEntry(array &$entry, array $data, $action)
{
$entry['code'] = $data['code'];
$entry['title'] = $data['title'];
$entry['rate'] = (float)$data['rate'];
$entry['sym'] = $data['sym'];
$entry['sym_pos'] = $data['sym_pos'];
$entry['default'] = (int)$data['default'];
$requiredFields = ['code', 'title', 'rate', 'sym'];
foreach ($requiredFields as $fieldName) {
if (empty($entry[$fieldName])) {
$this->addMessage(iaLanguage::getf('field_is_empty', ['field' => iaLanguage::get($fieldName)]), false);
}
}
return !$this->getMessages();
}
}
\ No newline at end of file
<?php
//##copyright##
if (iaView::REQUEST_HTML == $iaView->getRequestType()) {
if (1 != count($iaCore->requestPath)) {
return iaView::errorPage(iaView::ERROR_NOT_FOUND);
}
$iaCurrency = $iaCore->factoryModule('currency', IA_CURRENT_MODULE);
$iaCurrency->set($iaCore->requestPath[0]);
$redirectUrl = isset($_SERVER['HTTP_REFERER'])
? $_SERVER['HTTP_REFERER']
: IA_URL;
iaUtil::go_to($redirectUrl);
}
\ No newline at end of file
......@@ -97,8 +97,8 @@ class iaCart extends abstractModuleFront
return false;
}
$stmt = '`cart_id` = :cart AND `product_id` = :product AND `price` = :price';
$this->iaDb->bind($stmt, ['cart' => $cart->id, 'product' => $productId, 'price' => $product['price']]);
$stmt = '`cart_id` = :cart AND `product_id` = :product';
$this->iaDb->bind($stmt, ['cart' => $cart->id, 'product' => $productId]);
if ($this->iaDb->exists($stmt)) {
$result = (bool)$this->iaDb->update(null, $stmt, ['qty' => '`qty` + ' . (int)$quantity]);
......
<?php
//##copyright##
class iaCurrency extends abstractModuleFront
{
const CACHE_KEY = 'commerce_currencies';
const SESSION_KEY = 'cmrc_currency';
protected static $_table = 'currencies';
protected $_moduleName = 'commerce';
protected $_iaCache;
public function init()
{
parent::init();
$this->_iaCache = $this->iaCore->factory('cache');
}
public function getByCode($code)
{
$entries = $this->fetch();
return isset($entries[$code]) ? $entries[$code] : null;
}
public function fetch()
{
$result = $this->_iaCache->get(self::CACHE_KEY, 604800, true);
if (false === $result) {
$result = $this->_fetchFromDb();
$this->_iaCache->write(self::CACHE_KEY, $result);
}
return $result;
}
public function get()
{
if (isset($_SESSION[self::SESSION_KEY])) {
return $_SESSION[self::SESSION_KEY];
} else {
foreach ($this->fetch() as $entry) {
if ($entry['default']) {
$this->set($entry['code']);
break;
}
}
return $entry;
}
}
public function set($currencyCode)
{
if ($currency = $this->getByCode($currencyCode)) {
$_SESSION[self::SESSION_KEY] = $currency;
}
}
public function format($number)
{
$currency = $this->get();
$converted = $this->iaCore->get('currency') != $currency['code']
? $number * $currency['rate']
: $number;
$converted = number_format($converted, 2);
$prefix = ('pre' == $currency['sym_pos']) ? $currency['sym'] : '';
$postfix = ('post' == $currency['sym_pos']) ? $currency['sym'] : '';
return $prefix . $converted . $postfix;
}
protected function _fetchFromDb()
{
$rows = $this->iaDb->all(iaDb::ALL_COLUMNS_SELECTION, '1 ORDER BY `order`', null, null, self::getTable());
$result = [];
foreach ($rows as $row) {
unset($row['order']);
$row['rate'] = (float)$row['rate'];
$row['default'] = (bool)$row['default'];
$result[$row['code']] = $row;
}
return $result;
}
public function refreshRates()
{
$provider = $this->_instantiateProvider($this->iaCore->get('commerce_rates_provider'));
if ($provider) {
if ($rates = $provider->fetch()) {
$this->iaDb->setTable(self::getTable());
foreach ($rates as $currencyCode => $rate) {
$this->iaDb->update(['rate' => $rate], iaDb::convertIds($currencyCode, 'code'));
}
$this->iaDb->resetTable();
}
}
}
protected function _instantiateProvider($name)
{
$class = 'iaRatesProvider' . ucfirst($name);
$file = IA_MODULES . $this->getModuleName() . '/includes/providers/' . $name . iaSystem::EXECUTABLE_FILE_EXT;
if (file_exists($file)) {
require_once $file;
if (class_exists($class)) {
$instance = new $class($this);
$instance->init();
return $instance;
}
}
return false;
}
}
\ No newline at end of file
......@@ -146,6 +146,26 @@ class iaProduct extends abstractModuleFront
return $this->_foundRows;
}
protected function _processValues(&$rows, $singleRow = false, $fieldNames = [])
{
parent::_processValues($rows, $singleRow, $fieldNames);
if ($rows) {
$singleRow && $rows = [$rows];
$iaCurrency = $this->iaCore->factoryModule('currency', $this->getModuleName());
$currency = $iaCurrency->get();
foreach ($rows as &$row) {
$row['price_formatted'] = $iaCurrency->format($row['price']);
$row['currency'] = $currency;
}
$singleRow && $rows = array_shift($rows);
}
}
public function getSortingOptions()
{
return array_keys($this->_sortingOptions);
......
<?php
//##copyright##
$iaCore->factoryModule('currency', 'commerce')->refreshRates();
\ No newline at end of file
......@@ -4,8 +4,8 @@
if (iaView::REQUEST_HTML == $iaView->getRequestType()) {
$iaView->add_js('_IA_URL_modules/commerce/js/commerce, _IA_URL_modules/commerce/js/app');
$iaCategory = $iaCore->factoryModule('category', 'commerce');
$iaProduct = $iaCore->factoryModule('product', $iaCategory->getModuleName());
$iaProduct = $iaCore->factoryModule('product', 'commerce');
$iaCategory = $iaCore->factoryModule('category', $iaProduct->getModuleName());
$blocksData = ['itemName' => $iaProduct->getItemName()];
......@@ -19,6 +19,12 @@ if (iaView::REQUEST_HTML == $iaView->getRequestType()) {
];
}
if ($iaView->blockExists('currencies')) {
$iaCurrency = $iaCore->factoryModule('currency', $iaProduct->getModuleName());
$blocksData['currencies'] = $iaCurrency->fetch();
}
if ($iaView->blockExists('priority_categories')) {
$blocksData['priorityCategories'] = $iaCategory->getBlockPriority();
}
......
<?php
//##copyright##
class iaRatesProviderJsonrates extends abstractCore
{
const ENDPOINT_URL = 'http://apilayer.net/api/live';
protected $_iaCurrency;
public function __construct($iaCurrency)
{
$this->_iaCurrency = $iaCurrency;
}
protected function _composeUrl($source, $currencies)
{
$params = [
'access_key' => $this->iaCore->get('commerce_apikey_jsonrates'),
'source' => $source,
'currencies' => implode(',', $currencies),
'format' => 1
];
return self::ENDPOINT_URL . '?' . http_build_query($params);
}
public function fetch()
{
$source = null;
$currencies = [];
foreach ($this->_iaCurrency->fetch() as $currency) {
if ($currency['default']) {
$source = $currency['code'];
} else {
$currencies[] = $currency['code'];
}
}
$url = $this->_composeUrl($source, $currencies);
$response = $this->_httpRequest($url);
if ($response) {
$array = json_decode($response, true);
if (!empty($array['quotes'])) {
$rates = [];
foreach ($array['quotes'] as $cur => $rate) {
$code = substr($cur, 3);
$rate = (float)$rate;
$rates[$code] = $rate;
}
return $rates;
}
}
return false;
}
protected function _httpRequest($url)
{
return iaUtil::getPageContent($url);
}
}
\ No newline at end of file
......@@ -25,6 +25,9 @@
<action name="products_list" url="commerce/products/" icon="list" pages="commerce_stats,products:add,products:edit">List</action>
<action name="products_add" url="commerce/products/add/" icon="plus-alt" pages="products,products:edit">New Product</action>
<action name="currencies_list" url="commerce/currencies/" icon="list" pages="commerce_stats,currencies:add,currencies:edit">List</action>
<action name="currencies_add" url="commerce/currencies/add/" icon="plus-alt" pages="currencies,currencies:edit">New Currency</action>
</actions>
<groups>
......@@ -33,6 +36,8 @@
<adminpages>
<page group="commerce" name="commerce_stats" url="commerce/" filename="statistics" menus="menu">Statistics</page>
<page group="commerce" menus="menu">Settings</page>
<page group="commerce" name="currencies" url="commerce/currencies/" filename="currencies" menus="menu">Currencies</page>
<page group="commerce" menus="menu">Orders</page>
<page group="commerce" name="orders" url="commerce/orders/" filename="orders" menus="menu">Orders</page>
<page group="commerce" menus="menu">Content</page>
......@@ -46,6 +51,7 @@
<page group="commerce" name="view_product" url="|PACKAGE|product/" filename="view" readonly="1">Product Details</page>
<page group="commerce" name="cart" url="|PACKAGE|cart/" filename="cart" readonly="1">Cart</page>
<page group="commerce" name="checkout" url="|PACKAGE|checkout/" filename="checkout" readonly="1">Checkout</page>
<page group="commerce" name="currency" url="|PACKAGE|currency/" filename="currency" service="1">Currencies</page>
</pages>
<permissions>
......@@ -58,6 +64,9 @@
</permissions>
<configgroup name="commerce">Commerce</configgroup>
<config group="commerce" type="divider">General</config>
<config group="commerce" name="commerce_rates_provider" values="jsonrates" type="select" description="Exchange rates provider">jsonrates</config>
<config group="commerce" name="commerce_apikey_jsonrates" type="text" description="jsonrates API Key" show="commerce_rates_provider|jsonrates"><![CDATA[f0419431ff596992b212b68a6df57d80]]></config>
<config group="commerce" type="divider">Limits</config>
<config group="commerce" name="commerce_products_per_page" type="text" description="Products per page">20</config>
<config group="commerce" name="commerce_featured_products_limit" type="text" description="Products in 'Featured Products' block" private="1">10</config>
......@@ -205,17 +214,24 @@
<field item="orders" group="other" name="comment" type="textarea" editable="0" length="5000" page="checkout">Comment</field>
</fields>
<cron name="Exchange rates update">0 0 0/12 1/1 * modules/commerce/includes/cron/exchange-rates.php</cron>
<phrases>
<phrase category="admin" key="add_category">Add Category</phrase>
<phrase category="admin" key="add_currency">Add Currency</phrase>
<phrase category="admin" key="add_product">Add Product</phrase>
<phrase category="admin" key="buyer">Buyer</phrase>
<phrase category="admin" key="commerce_package">&quot;Commerce&quot; Package</phrase>
<phrase category="admin" key="currency_symbol">Currency symbol</phrase>
<phrase category="admin" key="do_you_want_to_delete_selected_products">Do you really want to delete MULTIPLE products permanently?</phrase>
<phrase category="admin" key="do_you_want_to_delete_this_category">Do you really want to delete this category and all of its subcategories?</phrase>
<phrase category="admin" key="do_you_want_to_delete_this_product">Do you really want to delete this product permanently?</phrase>
<phrase category="admin" key="do_you_want_to_remove_order">Do you really want to permanently remove this order?</phrase>
<phrase category="admin" key="edit_category">Edit Category</phrase>
<phrase category="admin" key="edit_currency">Edit Currency ":title"</phrase>
<phrase category="admin" key="edit_product">Edit Product</phrase>
<phrase category="admin" key="exchange_rate">Exchange rate</phrase>
<phrase category="admin" key="rate">Rate</phrase>
<phrase category="admin" key="order_removed">Order removed.</phrase>
<phrase category="admin" key="priority">Priority</phrase>
<phrase category="admin" key="product_with_similar_slug_exists_in_category">Product with the same slug already exists in category.</phrase>
......@@ -254,6 +270,7 @@
<blocks>
<block name="cart" title="Shopping Cart" position="account" type="smarty" header="0" filename="block.cart"><![CDATA[]]></block>
<block name="currencies" title="Currencies" position="account" type="smarty" header="0" filename="block.currencies"><![CDATA[]]></block>
<block name="subcategories" title="Subcategories" position="top" type="smarty" header="0" sticky="0" pages="storefront" filename="block.subcategories"><![CDATA[]]></block>
<block name="priority_categories" title="Priority Categories" position="right" type="smarty" header="1" filename="block.priority-categories"><![CDATA[]]></block>
<block name="featured_products" title="Featured Products" position="verytop" type="smarty" sticky="1" filename="block.featured-products"><![CDATA[]]></block>
......@@ -381,9 +398,32 @@ CREATE TABLE `{prefix}orders_items` (
) {mysql_version};
]]>
</sql>
<sql>
<![CDATA[
CREATE TABLE `{prefix}currencies` (
`code` char(3) NOT NULL,
`title` varchar(30) NOT NULL,
`rate` decimal(7,2) unsigned NOT NULL,
`sym` varchar(5) NOT NULL,
`sym_pos` enum('pre', 'post') NOT NULL default 'pre',
`default` tinyint(1) unsigned NOT NULL,
`order` tinyint(2) unsigned NOT NULL,
PRIMARY KEY (`code`)
) {mysql_version};
]]>
</sql>
<sql>
<![CDATA[
INSERT INTO `{prefix}currencies` VALUES
('USD', 'US Dollar', 1, '$', 'pre', 1, 1),
('EUR', 'Euro', 0.87, '€', 'post', 0, 2),
('GBP', 'Pound Sterling', 0.78, '£', 'pre', 0, 3);
]]>
</sql>
<code>
<![CDATA[
$iaCore->factoryModule('category', 'commerce', iaCore::ADMIN)->setupDbStructure();
$iaCore->factoryModule('currency', 'commerce')->refreshRates();
]]>
</code>
</install>
......@@ -392,7 +432,7 @@ $iaCore->factoryModule('category', 'commerce', iaCore::ADMIN)->setupDbStructure(
<sql>
<![CDATA[
DROP TABLE IF EXISTS `{prefix}products`, `{prefix}categories`, `{prefix}categories_flat`,
`{prefix}cart`, `{prefix}cart_items`, `{prefix}orders`, `{prefix}orders_items`;
`{prefix}cart`, `{prefix}cart_items`, `{prefix}orders`, `{prefix}orders_items`, `{prefix}currencies`;
]]>
</sql>
</uninstall>
......
Ext.onReady(function () {
if (Ext.get('js-grid-placeholder')) {
var grid = new IntelliGrid({
columns: [
'selection',
{name: 'code', title: _t('code'), width: 100, editor: 'text'},
{name: 'title', title: _t('title'), width: 1, editor: 'text'},
{name: 'rate', title: _t('rate'), width: 70},
{name: 'default', title: _t('default'), width: 70, renderer: intelli.gridHelper.renderer.check},
{name: 'order', title: _t('order'), width: 60, editor: 'number'},
'update',
'delete'
]
}, false);
grid.toolbar = new Ext.Toolbar({items: [
{
emptyText: _t('title'),
listeners: intelli.gridHelper.listener.specialKey,
name: 'title',
width: 250,
xtype: 'textfield'
}, {
displayField: 'title',
editable: false,
emptyText: _t('status'),
name: 'status',
store: grid.stores.statuses,
typeAhead: true,
valueField: 'value',
width: 100,
xtype: 'combo'
}, {
handler: function () {
intelli.gridHelper.search(grid)
},
id: 'fltBtn',
text: '<i class="i-search"></i> ' + _t('search')
}, {
handler: function () {
intelli.gridHelper.search(grid, true)
},
text: '<i class="i-close"></i> ' + _t('reset')
}]
});
grid.init();
}
});
\ No newline at end of file
......@@ -35,6 +35,12 @@ $(function(){
}})
})
$('a', '#js-cmrc-currencies').on('click', function(e) {
e.preventDefault()
window.location = intelli.config.packages.commerce.url + 'currency/' + $(this).data('code') + '/';
})
$('#js-sorting-selector').on('change', function(){
intelli.commerce.utils.insertUriParam('sorting', $('option:selected', this).val())
})
......
<form method="post" enctype="multipart/form-data" class="sap-form form-horizontal">
{preventCsrf}
<div class="wrap-list">
<div class="wrap-group">
<div class="wrap-group-heading">{lang key='general'}</div>
<div class="row">
<div class="col col-lg-2">
<label class="control-label" for="input-code">{lang key='code'} {lang key='field_required'}</label>
</div>
<div class="col col-lg-4">
<input type="text" name="code" value="{$item.code|escape}" id="input-code" maxlength="3">
</div>
</div>
<div class="row">
<div class="col col-lg-2">
<label class="control-label" for="input-title">{lang key='title'} {lang key='field_required'}</label>
</div>
<div class="col col-lg-4">
<input type="text" name="title" value="{$item.title|escape}" id="input-title" maxlength="30">
</div>
</div>
</div>
<div class="wrap-group">
<div class="wrap-group-heading">{lang key='rate'}</div>
<div class="row">
<div class="col col-lg-2">
<label class="control-label" for="input-rate">{lang key='exchange_rate'} {lang key='field_required'}</label>
</div>
<div class="col col-lg-4">
<input type="text" name="rate" value="{$item.rate}" id="input-rate" maxlength="8">
</div>
</div>
</div>
<div class="wrap-group">
<div class="wrap-group-heading">{lang key='settings'}</div>
<div class="row">
<div class="col col-lg-2">
<label class="control-label" for="input-sym">{lang key='currency_symbol'} {lang key='field_required'}</label>
</div>
<div class="col col-lg-4">
<input type="text" name="sym" value="{$item.sym|escape}" id="input-sym" maxlength="5">
<select name="sym_pos">
<option value="pre"{if 'pre' == $item.sym_pos} selected{/if}>before the price</option>
<option value="post"{if 'post' == $item.sym_pos} selected{/if}>after the price</option>
</select>
</div>
</div>
<div class="row">
<div class="col col-lg-2">
<label class="control-label" for="input-default">{lang key='default'}</label>
</div>
<div class="col col-lg-4">
{html_radio_switcher value=$item.default name='default'}
</div>
</div>
</div>
</div>
{include 'field-type-content-fieldset.tpl' isSystem=true noSystemFields=true}
</form>
{ia_add_media files='js:_IA_URL_modules/commerce/js/admin/categories'}
\ No newline at end of file
<ul id="js-cmrc-currencies">
{foreach $commerce.currencies as $currency}
<li{if $currency.default} class="active"{/if}><a href="#" data-code="{$currency.code}">{$currency.title|escape}</a></li>
{/foreach}
</ul>
\ No newline at end of file
......@@ -6,7 +6,7 @@
<div class="card__body">
<div class="card__body__title">{ia_url item='products' type='link' data=$item text=$item.title}</div>
<div class="card__body__snippet">{$item.snippet|truncate:50:'...':true}</div>
<div class="card__body__price">{$item.price|number_format} {$core.config.currency}</div>
<div class="card__body__price">{$item.price_formatted}</div>
<div class="card__body__actions">
<a href="#" class="js-cmd-quick-view" data-id="{$item.id}"><span class="fa fa-search"></span></a>
<button type="button" class="js-cmd-add-cart" data-id="{$item.id}">{lang key='add_to_cart'}</button>
......
......@@ -20,8 +20,8 @@
</div>
<div class="pull-right">
<p class="lead" itemprop="offers" itemscope itemtype="http://schema.org/Offer">
<span itemprop="price" content="{$item.price}">{$item.price|number_format}</span>
<span itemprop="priceCurrency">{$core.config.currency}</span>
<span itemprop="price" content="{$item.price}">{$item.price_formatted}</span>
<span itemprop="priceCurrency">{$item.currency}</span>
</p>
</div>
......
......@@ -11,7 +11,7 @@
{if $item.pictures}
<div class="well">
{foreach $item.pictures as $picture}
{ia_image file=$item.picture type='thumbnail' width=100}
{ia_image file=$picture type='thumbnail' width=100}
{/foreach}
</div>
{/if}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment