mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +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:
		| @@ -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 | ||||||
|  */ |  */ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user