mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Client side currency conversion (#4293)
* Automatically update exchange rates when base currency is updated * Adds API endpoint with currency exchange information * Add unit testing for new endpoint * Implement javascript code for client-side conversion * Adds helper function for calculating total price of a dataset * javascript cleanup * Add functionality to sales order tables * JS linting * Update API version * Prevent auto currency updates under certain conditions
This commit is contained in:
parent
9a289948e5
commit
eccd3be150
@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 91
|
INVENTREE_API_VERSION = 92
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v92 -> 2023-02-02 : https://github.com/inventree/InvenTree/pull/4293
|
||||||
|
- Adds API endpoint for currency exchange information
|
||||||
|
|
||||||
v91 -> 2023-01-31 : https://github.com/inventree/InvenTree/pull/4281
|
v91 -> 2023-01-31 : https://github.com/inventree/InvenTree/pull/4281
|
||||||
- Improves the API endpoint for creating new Part instances
|
- Improves the API endpoint for creating new Part instances
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ from django.views.decorators.csrf import csrf_exempt
|
|||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django_q.tasks import async_task
|
from django_q.tasks import async_task
|
||||||
|
from djmoney.contrib.exchange.models import Rate
|
||||||
from rest_framework import filters, permissions, serializers
|
from rest_framework import filters, permissions, serializers
|
||||||
from rest_framework.exceptions import NotAcceptable, NotFound
|
from rest_framework.exceptions import NotAcceptable, NotFound
|
||||||
from rest_framework.permissions import IsAdminUser
|
from rest_framework.permissions import IsAdminUser
|
||||||
@ -102,6 +103,36 @@ class WebhookView(CsrfExemptMixin, APIView):
|
|||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
|
|
||||||
|
class CurrencyExchangeView(APIView):
|
||||||
|
"""API endpoint for displaying currency information
|
||||||
|
|
||||||
|
TODO: Add a POST hook to refresh / update the currency exchange data
|
||||||
|
"""
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
permissions.IsAuthenticated,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, format=None):
|
||||||
|
"""Return information on available currency conversions"""
|
||||||
|
|
||||||
|
# Extract a list of all available rates
|
||||||
|
try:
|
||||||
|
rates = Rate.objects.all()
|
||||||
|
except Exception:
|
||||||
|
rates = []
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'base_currency': common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', 'USD'),
|
||||||
|
'exchange_rates': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
for rate in rates:
|
||||||
|
response['exchange_rates'][rate.currency] = rate.value
|
||||||
|
|
||||||
|
return Response(response)
|
||||||
|
|
||||||
|
|
||||||
class SettingsList(ListAPI):
|
class SettingsList(ListAPI):
|
||||||
"""Generic ListView for settings.
|
"""Generic ListView for settings.
|
||||||
|
|
||||||
@ -418,6 +449,11 @@ common_api_urls = [
|
|||||||
# Webhooks
|
# Webhooks
|
||||||
path('webhook/<slug:endpoint>/', WebhookView.as_view(), name='api-webhook'),
|
path('webhook/<slug:endpoint>/', WebhookView.as_view(), name='api-webhook'),
|
||||||
|
|
||||||
|
# Currencies
|
||||||
|
re_path(r'^currency/', include([
|
||||||
|
re_path(r'^exchange/', CurrencyExchangeView.as_view(), name='api-currency-exchange'),
|
||||||
|
])),
|
||||||
|
|
||||||
# Notifications
|
# Notifications
|
||||||
re_path(r'^notifications/', include([
|
re_path(r'^notifications/', include([
|
||||||
# Individual purchase order detail URLs
|
# Individual purchase order detail URLs
|
||||||
|
@ -43,6 +43,7 @@ import build.validators
|
|||||||
import InvenTree.fields
|
import InvenTree.fields
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import InvenTree.ready
|
import InvenTree.ready
|
||||||
|
import InvenTree.tasks
|
||||||
import InvenTree.validators
|
import InvenTree.validators
|
||||||
import order.validators
|
import order.validators
|
||||||
|
|
||||||
@ -821,6 +822,18 @@ def validate_email_domains(setting):
|
|||||||
raise ValidationError(_(f'Invalid domain name: {domain}'))
|
raise ValidationError(_(f'Invalid domain name: {domain}'))
|
||||||
|
|
||||||
|
|
||||||
|
def update_exchange_rates(setting):
|
||||||
|
"""Update exchange rates when base currency is changed"""
|
||||||
|
|
||||||
|
if InvenTree.ready.isImportingData():
|
||||||
|
return
|
||||||
|
|
||||||
|
if not InvenTree.ready.canAppAccessDatabase():
|
||||||
|
return
|
||||||
|
|
||||||
|
InvenTree.tasks.update_exchange_rates()
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeSetting(BaseInvenTreeSetting):
|
class InvenTreeSetting(BaseInvenTreeSetting):
|
||||||
"""An InvenTreeSetting object is a key:value pair used for storing single values (e.g. one-off settings values).
|
"""An InvenTreeSetting object is a key:value pair used for storing single values (e.g. one-off settings values).
|
||||||
|
|
||||||
@ -901,9 +914,10 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
|
|
||||||
'INVENTREE_DEFAULT_CURRENCY': {
|
'INVENTREE_DEFAULT_CURRENCY': {
|
||||||
'name': _('Default Currency'),
|
'name': _('Default Currency'),
|
||||||
'description': _('Default currency'),
|
'description': _('Select base currency for pricing caluclations'),
|
||||||
'default': 'USD',
|
'default': 'USD',
|
||||||
'choices': CURRENCY_CHOICES,
|
'choices': CURRENCY_CHOICES,
|
||||||
|
'after_save': update_exchange_rates,
|
||||||
},
|
},
|
||||||
|
|
||||||
'INVENTREE_DOWNLOAD_FROM_URL': {
|
'INVENTREE_DOWNLOAD_FROM_URL': {
|
||||||
|
@ -900,3 +900,15 @@ class ColorThemeTest(TestCase):
|
|||||||
# check valid theme
|
# check valid theme
|
||||||
self.assertFalse(ColorTheme.is_valid_choice(aa))
|
self.assertFalse(ColorTheme.is_valid_choice(aa))
|
||||||
self.assertTrue(ColorTheme.is_valid_choice(ab))
|
self.assertTrue(ColorTheme.is_valid_choice(ab))
|
||||||
|
|
||||||
|
|
||||||
|
class CurrencyAPITests(InvenTreeAPITestCase):
|
||||||
|
"""Unit tests for the currency exchange API endpoints"""
|
||||||
|
|
||||||
|
def test_exchange_endpoint(self):
|
||||||
|
"""Test that the currency exchange endpoint works as expected"""
|
||||||
|
|
||||||
|
response = self.get(reverse('api-currency-exchange'), expected_code=200)
|
||||||
|
|
||||||
|
self.assertIn('base_currency', response.data)
|
||||||
|
self.assertIn('exchange_rates', response.data)
|
||||||
|
@ -4,9 +4,7 @@
|
|||||||
blankImage,
|
blankImage,
|
||||||
deleteButton,
|
deleteButton,
|
||||||
editButton,
|
editButton,
|
||||||
formatCurrency,
|
|
||||||
formatDecimal,
|
formatDecimal,
|
||||||
formatPriceRange,
|
|
||||||
imageHoverIcon,
|
imageHoverIcon,
|
||||||
makeIconBadge,
|
makeIconBadge,
|
||||||
makeIconButton,
|
makeIconButton,
|
||||||
@ -40,74 +38,6 @@ function deleteButton(url, text='{% trans "Delete" %}') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* format currency (money) value based on current settings
|
|
||||||
*
|
|
||||||
* Options:
|
|
||||||
* - currency: Currency code (uses default value if none provided)
|
|
||||||
* - locale: Locale specified (uses default value if none provided)
|
|
||||||
* - digits: Maximum number of significant digits (default = 10)
|
|
||||||
*/
|
|
||||||
function formatCurrency(value, options={}) {
|
|
||||||
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var digits = options.digits || global_settings.PRICING_DECIMAL_PLACES || 6;
|
|
||||||
|
|
||||||
// Strip out any trailing zeros, etc
|
|
||||||
value = formatDecimal(value, digits);
|
|
||||||
|
|
||||||
// Extract default currency information
|
|
||||||
var currency = options.currency || global_settings.INVENTREE_DEFAULT_CURRENCY || 'USD';
|
|
||||||
|
|
||||||
// Exctract locale information
|
|
||||||
var locale = options.locale || navigator.language || 'en-US';
|
|
||||||
|
|
||||||
|
|
||||||
var formatter = new Intl.NumberFormat(
|
|
||||||
locale,
|
|
||||||
{
|
|
||||||
style: 'currency',
|
|
||||||
currency: currency,
|
|
||||||
maximumSignificantDigits: digits,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return formatter.format(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Format a range of prices
|
|
||||||
*/
|
|
||||||
function formatPriceRange(price_min, price_max, options={}) {
|
|
||||||
|
|
||||||
var p_min = price_min || price_max;
|
|
||||||
var p_max = price_max || price_min;
|
|
||||||
|
|
||||||
var quantity = options.quantity || 1;
|
|
||||||
|
|
||||||
if (p_min == null && p_max == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
p_min = parseFloat(p_min) * quantity;
|
|
||||||
p_max = parseFloat(p_max) * quantity;
|
|
||||||
|
|
||||||
var output = '';
|
|
||||||
|
|
||||||
output += formatCurrency(p_min, options);
|
|
||||||
|
|
||||||
if (p_min != p_max) {
|
|
||||||
output += ' - ';
|
|
||||||
output += formatCurrency(p_max, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Ensure a string does not exceed a maximum length.
|
* Ensure a string does not exceed a maximum length.
|
||||||
|
@ -2396,17 +2396,15 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
footerFormatter: function(data) {
|
footerFormatter: function(data) {
|
||||||
var total = data.map(function(row) {
|
return calculateTotalPrice(
|
||||||
return +row['purchase_price']*row['quantity'];
|
data,
|
||||||
}).reduce(function(sum, i) {
|
function(row) {
|
||||||
return sum + i;
|
return row.purchase_price ? row.purchase_price * row.quantity : null;
|
||||||
}, 0);
|
},
|
||||||
|
function(row) {
|
||||||
var currency = (data.slice(-1)[0] && data.slice(-1)[0].purchase_price_currency) || 'USD';
|
return row.purchase_price_currency;
|
||||||
|
}
|
||||||
return formatCurrency(total, {
|
);
|
||||||
currency: currency
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2583,17 +2581,15 @@ function loadPurchaseOrderExtraLineTable(table, options={}) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
footerFormatter: function(data) {
|
footerFormatter: function(data) {
|
||||||
var total = data.map(function(row) {
|
return calculateTotalPrice(
|
||||||
return +row['price'] * row['quantity'];
|
data,
|
||||||
}).reduce(function(sum, i) {
|
function(row) {
|
||||||
return sum + i;
|
return row.price ? row.price * row.quantity : null;
|
||||||
}, 0);
|
},
|
||||||
|
function(row) {
|
||||||
var currency = (data.slice(-1)[0] && data.slice(-1)[0].price_currency) || 'USD';
|
return row.price_currency;
|
||||||
|
}
|
||||||
return formatCurrency(total, {
|
);
|
||||||
currency: currency,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -3908,17 +3904,15 @@ function loadSalesOrderLineItemTable(table, options={}) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
footerFormatter: function(data) {
|
footerFormatter: function(data) {
|
||||||
var total = data.map(function(row) {
|
return calculateTotalPrice(
|
||||||
return +row['sale_price'] * row['quantity'];
|
data,
|
||||||
}).reduce(function(sum, i) {
|
function(row) {
|
||||||
return sum + i;
|
return row.sale_price ? row.sale_price * row.quantity : null;
|
||||||
}, 0);
|
},
|
||||||
|
function(row) {
|
||||||
var currency = (data.slice(-1)[0] && data.slice(-1)[0].sale_price_currency) || 'USD';
|
return row.sale_price_currency;
|
||||||
|
}
|
||||||
return formatCurrency(total, {
|
);
|
||||||
currency: currency,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -4399,17 +4393,15 @@ function loadSalesOrderExtraLineTable(table, options={}) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
footerFormatter: function(data) {
|
footerFormatter: function(data) {
|
||||||
var total = data.map(function(row) {
|
return calculateTotalPrice(
|
||||||
return +row['price'] * row['quantity'];
|
data,
|
||||||
}).reduce(function(sum, i) {
|
function(row) {
|
||||||
return sum + i;
|
return row.price ? row.price * row.quantity : null;
|
||||||
}, 0);
|
},
|
||||||
|
function(row) {
|
||||||
var currency = (data.slice(-1)[0] && data.slice(-1)[0].price_currency) || 'USD';
|
return row.price_currency;
|
||||||
|
}
|
||||||
return formatCurrency(total, {
|
);
|
||||||
currency: currency,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
@ -7,6 +7,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* exported
|
/* exported
|
||||||
|
baseCurrency,
|
||||||
|
calculateTotalPrice,
|
||||||
|
convertCurrency,
|
||||||
|
formatCurrency,
|
||||||
|
formatPriceRange,
|
||||||
loadBomPricingChart,
|
loadBomPricingChart,
|
||||||
loadPartSupplierPricingTable,
|
loadPartSupplierPricingTable,
|
||||||
initPriceBreakSet,
|
initPriceBreakSet,
|
||||||
@ -17,6 +22,242 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns the base currency used for conversion operations
|
||||||
|
*/
|
||||||
|
function baseCurrency() {
|
||||||
|
return global_settings.INVENTREE_BASE_CURRENCY || 'USD';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* format currency (money) value based on current settings
|
||||||
|
*
|
||||||
|
* Options:
|
||||||
|
* - currency: Currency code (uses default value if none provided)
|
||||||
|
* - locale: Locale specified (uses default value if none provided)
|
||||||
|
* - digits: Maximum number of significant digits (default = 10)
|
||||||
|
*/
|
||||||
|
function formatCurrency(value, options={}) {
|
||||||
|
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var digits = options.digits || global_settings.PRICING_DECIMAL_PLACES || 6;
|
||||||
|
|
||||||
|
// Strip out any trailing zeros, etc
|
||||||
|
value = formatDecimal(value, digits);
|
||||||
|
|
||||||
|
// Extract default currency information
|
||||||
|
var currency = options.currency || global_settings.INVENTREE_DEFAULT_CURRENCY || 'USD';
|
||||||
|
|
||||||
|
// Exctract locale information
|
||||||
|
var locale = options.locale || navigator.language || 'en-US';
|
||||||
|
|
||||||
|
|
||||||
|
var formatter = new Intl.NumberFormat(
|
||||||
|
locale,
|
||||||
|
{
|
||||||
|
style: 'currency',
|
||||||
|
currency: currency,
|
||||||
|
maximumSignificantDigits: digits,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return formatter.format(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Format a range of prices
|
||||||
|
*/
|
||||||
|
function formatPriceRange(price_min, price_max, options={}) {
|
||||||
|
|
||||||
|
var p_min = price_min || price_max;
|
||||||
|
var p_max = price_max || price_min;
|
||||||
|
|
||||||
|
var quantity = options.quantity || 1;
|
||||||
|
|
||||||
|
if (p_min == null && p_max == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
p_min = parseFloat(p_min) * quantity;
|
||||||
|
p_max = parseFloat(p_max) * quantity;
|
||||||
|
|
||||||
|
var output = '';
|
||||||
|
|
||||||
|
output += formatCurrency(p_min, options);
|
||||||
|
|
||||||
|
if (p_min != p_max) {
|
||||||
|
output += ' - ';
|
||||||
|
output += formatCurrency(p_max, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: Implement a better version of caching here
|
||||||
|
var cached_exchange_rates = null;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Retrieve currency conversion rate information from the server
|
||||||
|
*/
|
||||||
|
function getCurrencyConversionRates() {
|
||||||
|
|
||||||
|
if (cached_exchange_rates != null) {
|
||||||
|
return cached_exchange_rates;
|
||||||
|
}
|
||||||
|
|
||||||
|
inventreeGet('{% url "api-currency-exchange" %}', {}, {
|
||||||
|
async: false,
|
||||||
|
success: function(response) {
|
||||||
|
cached_exchange_rates = response;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return cached_exchange_rates;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Calculate the total price for a given dataset.
|
||||||
|
* Within each 'row' in the dataset, the 'price' attribute is denoted by 'key' variable
|
||||||
|
*
|
||||||
|
* The target currency is determined as follows:
|
||||||
|
* 1. Provided as options.currency
|
||||||
|
* 2. All rows use the same currency (defaults to this)
|
||||||
|
* 3. Use the result of baseCurrency function
|
||||||
|
*/
|
||||||
|
function calculateTotalPrice(dataset, value_func, currency_func, options={}) {
|
||||||
|
|
||||||
|
var currency = options.currency;
|
||||||
|
|
||||||
|
var rates = getCurrencyConversionRates();
|
||||||
|
|
||||||
|
if (!rates) {
|
||||||
|
console.error('Could not retrieve currency conversion information from the server');
|
||||||
|
return `<span class='icon-red fas fa-exclamation-circle' title='{% trans "Error fetching currency data" %}'></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currency) {
|
||||||
|
// Try to determine currency from the dataset
|
||||||
|
var common_currency = true;
|
||||||
|
|
||||||
|
for (var idx = 0; idx < dataset.length; idx++) {
|
||||||
|
var row = dataset[idx];
|
||||||
|
|
||||||
|
var row_currency = currency_func(row);
|
||||||
|
|
||||||
|
if (row_currency == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currency == null) {
|
||||||
|
currency = row_currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currency != row_currency) {
|
||||||
|
common_currency = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inconsistent currencies between rows - revert to base currency
|
||||||
|
if (!common_currency) {
|
||||||
|
currency = baseCurrency();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = null;
|
||||||
|
|
||||||
|
for (var ii = 0; ii < dataset.length; ii++) {
|
||||||
|
var row = dataset[ii];
|
||||||
|
|
||||||
|
// Pass the row back to the decoder
|
||||||
|
var value = value_func(row);
|
||||||
|
|
||||||
|
// Ignore null values
|
||||||
|
if (value == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to the desired currency
|
||||||
|
value = convertCurrency(
|
||||||
|
value,
|
||||||
|
currency_func(row) || baseCurrency(),
|
||||||
|
currency,
|
||||||
|
rates
|
||||||
|
);
|
||||||
|
|
||||||
|
if (value == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total is null until we get a good value
|
||||||
|
if (total == null) {
|
||||||
|
total = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
total += value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatCurrency(total, {
|
||||||
|
currency: currency,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert from one specified currency into another
|
||||||
|
*
|
||||||
|
* @param {number} value - numerical value
|
||||||
|
* @param {string} source_currency - The source currency code e.g. 'AUD'
|
||||||
|
* @param {string} target_currency - The target currency code e.g. 'USD'
|
||||||
|
* @param {object} rate_data - Currency exchange rate data received from the server
|
||||||
|
*/
|
||||||
|
function convertCurrency(value, source_currency, target_currency, rate_data) {
|
||||||
|
|
||||||
|
if (value == null) {
|
||||||
|
console.warn('Null value passed to convertCurrency function');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short circuit the case where the currencies are the same
|
||||||
|
if (source_currency == target_currency) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('base_currency' in rate_data)) {
|
||||||
|
console.error('Currency data missing base_currency parameter');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('exchange_rates' in rate_data)) {
|
||||||
|
console.error('Currency data missing exchange_rates parameter');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rates = rate_data['exchange_rates'];
|
||||||
|
|
||||||
|
if (!(source_currency in rates)) {
|
||||||
|
console.error(`Source currency '${source_currency}' not found in exchange rate data`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(target_currency in rates)) {
|
||||||
|
console.error(`Target currency '${target_currency}' not found in exchange rate date`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We assume that the 'base exchange rate' is 1:1
|
||||||
|
return value / rates[source_currency] * rates[target_currency];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Load BOM pricing chart
|
* Load BOM pricing chart
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user