From de89bf333969f1731a6d18bfe93cfee3690c3294 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 11 Feb 2026 09:11:56 +0700 Subject: [PATCH 1/2] [ADD] currency_rate_update_BI: Add Bank Indonesia currency rate provider Add new currency rate provider for Bank Indonesia (BI) Kurs Transaksi. This provider fetches official exchange rates from BI's webservice for 19 major currencies against IDR. Features: - Support for BI Kurs Transaksi webservice (getSubKursLokal3) - Handles 19 currencies published by Bank Indonesia - Calculates cross-rates for non-IDR base currencies - Proper XML parsing of BI's SOAP DataSet response Technical details: - Fetches buy/sell rates and computes middle rate - Handles nominal units (e.g., 100 for JPY) - Supports both IDR and foreign base currencies - Implements proper error handling and logging --- currency_rate_update_BI/README.rst | 175 ++++++ currency_rate_update_BI/__init__.py | 4 + currency_rate_update_BI/__manifest__.py | 17 + currency_rate_update_BI/hooks.py | 57 ++ .../i18n/currency_rate_update_BI.pot | 29 + currency_rate_update_BI/i18n/id.po | 31 + currency_rate_update_BI/models/__init__.py | 3 + .../models/res_currency_rate_provider_BI.py | 308 ++++++++++ currency_rate_update_BI/readme/CONFIGURE.rst | 14 + .../readme/DESCRIPTION.rst | 57 ++ currency_rate_update_BI/readme/USAGE.rst | 18 + .../static/description/icon.png | Bin 0 -> 83138 bytes .../static/description/index.html | 533 ++++++++++++++++++ currency_rate_update_BI/tests/__init__.py | 3 + .../tests/test_currency_rate_update_BI.py | 512 +++++++++++++++++ .../odoo/addons/currency_rate_update_BI | 1 + setup/currency_rate_update_BI/setup.py | 6 + 17 files changed, 1768 insertions(+) create mode 100644 currency_rate_update_BI/README.rst create mode 100644 currency_rate_update_BI/__init__.py create mode 100644 currency_rate_update_BI/__manifest__.py create mode 100644 currency_rate_update_BI/hooks.py create mode 100644 currency_rate_update_BI/i18n/currency_rate_update_BI.pot create mode 100644 currency_rate_update_BI/i18n/id.po create mode 100644 currency_rate_update_BI/models/__init__.py create mode 100644 currency_rate_update_BI/models/res_currency_rate_provider_BI.py create mode 100644 currency_rate_update_BI/readme/CONFIGURE.rst create mode 100644 currency_rate_update_BI/readme/DESCRIPTION.rst create mode 100644 currency_rate_update_BI/readme/USAGE.rst create mode 100644 currency_rate_update_BI/static/description/icon.png create mode 100644 currency_rate_update_BI/static/description/index.html create mode 100644 currency_rate_update_BI/tests/__init__.py create mode 100644 currency_rate_update_BI/tests/test_currency_rate_update_BI.py create mode 120000 setup/currency_rate_update_BI/odoo/addons/currency_rate_update_BI create mode 100644 setup/currency_rate_update_BI/setup.py diff --git a/currency_rate_update_BI/README.rst b/currency_rate_update_BI/README.rst new file mode 100644 index 00000000..71a22ffd --- /dev/null +++ b/currency_rate_update_BI/README.rst @@ -0,0 +1,175 @@ +==================================== +Currency Rate Update: Bank Indonesia +==================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:manual + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fcurrency-lightgray.png?logo=github + :target: https://github.com/OCA/currency/tree/16.0/currency_rate_update_BI + :alt: OCA/currency +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/currency-16-0/currency-16-0-currency_rate_update_BI + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/currency&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Adds **Bank Indonesia (BI)** as a currency rates provider for the +``currency_rate_update`` module. Exchange rates are fetched from +Bank Indonesia's official SOAP webservice (wsKursBI) using the +``getSubKursLokal3`` method. + +**Data Source:** Bank Indonesia — https://www.bi.go.id + +**Rate Type:** Kurs Transaksi BI (Transaction Rates — middle rate / kurs tengah) + +**Update Frequency:** Daily on business days (Mon–Fri, excluding Indonesian national holidays) + +**Supported Currencies (vs IDR):** + ++-----+----------------------------+ +| USD | United States Dollar | ++-----+----------------------------+ +| EUR | Euro | ++-----+----------------------------+ +| GBP | British Pound | ++-----+----------------------------+ +| JPY | Japanese Yen | ++-----+----------------------------+ +| SGD | Singapore Dollar | ++-----+----------------------------+ +| AUD | Australian Dollar | ++-----+----------------------------+ +| BND | Brunei Dollar | ++-----+----------------------------+ +| CAD | Canadian Dollar | ++-----+----------------------------+ +| CHF | Swiss Franc | ++-----+----------------------------+ +| CNY | Chinese Yuan | ++-----+----------------------------+ +| DKK | Danish Krone | ++-----+----------------------------+ +| HKD | Hong Kong Dollar | ++-----+----------------------------+ +| KRW | South Korean Won | ++-----+----------------------------+ +| MYR | Malaysian Ringgit | ++-----+----------------------------+ +| NOK | Norwegian Krone | ++-----+----------------------------+ +| NZD | New Zealand Dollar | ++-----+----------------------------+ +| SAR | Saudi Riyal | ++-----+----------------------------+ +| SEK | Swedish Krona | ++-----+----------------------------+ +| THB | Thai Baht | ++-----+----------------------------+ + +**Note:** This module is designed primarily for Indonesian companies +using **IDR (Indonesian Rupiah)** as their base currency. Cross-rate +calculations (e.g. USD base company wanting EUR rates) are supported +as long as the base currency is in the list above. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure Bank Indonesia currency rates provider: + +#. Go to *Invoicing > Configuration > Settings* +#. Ensure *Automatic Currency Rates (OCA)* is checked +#. Go to *Invoicing > Configuration > Currency Rates Providers* +#. Create a new provider with *Provider: Bank Indonesia (Kurs Transaksi)* +#. Select the currencies you want to update (USD, EUR, SGD, etc.) +#. Configure the update schedule (e.g., daily, weekly) +#. Save the provider + +.. note:: + Your company's base currency should be **IDR (Indonesian Rupiah)** + or one of the supported foreign currencies if you need cross-rate + calculations. + +Usage +===== + +After configuration, currency rates are automatically updated based on +your schedule. To manually update rates: + +#. Go to *Invoicing > Configuration > Currency Rates Providers* +#. Select the Bank Indonesia provider +#. Click *Action > Update Rates Wizard* +#. Set the date range (From/To dates) +#. Click *Update* to fetch rates from Bank Indonesia + +To view the fetched rates: + +#. Go to *Invoicing > Configuration > Currencies* +#. Select a currency (e.g., USD) +#. Open the *Rates* tab to see historical exchange rates + +The rates are fetched from Bank Indonesia's official webservice at +https://www.bi.go.id/biwebservice/wskursbi.asmx and represent the +middle rate (average of buy/sell rates) for each currency pair. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* PT Solusi Aglis Indonesia + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-hitrosol| image:: https://github.com/hitrosol.png?size=40px + :target: https://github.com/hitrosol + :alt: hitrosol + +Current `maintainer `__: + +|maintainer-hitrosol| + +This module is part of the `OCA/currency `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/currency_rate_update_BI/__init__.py b/currency_rate_update_BI/__init__.py new file mode 100644 index 00000000..4f71d542 --- /dev/null +++ b/currency_rate_update_BI/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from .hooks import post_init_hook +from . import models diff --git a/currency_rate_update_BI/__manifest__.py b/currency_rate_update_BI/__manifest__.py new file mode 100644 index 00000000..a290493f --- /dev/null +++ b/currency_rate_update_BI/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2026 PT Solusi Aglis Indonesia (http://solusiaglis.co.id) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "Currency Rate Update: Bank Indonesia", + "version": "16.0.1.0.0", + "category": "Financial Management/Configuration", + "summary": "Update exchange rates using Bank Indonesia (BI) official rates", + "author": "PT Solusi Aglis Indonesia, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/currency", + "license": "AGPL-3", + "installable": True, + "application": False, + "depends": ["currency_rate_update"], + "post_init_hook": "post_init_hook", + "maintainers": ["hitrosol"], +} diff --git a/currency_rate_update_BI/hooks.py b/currency_rate_update_BI/hooks.py new file mode 100644 index 00000000..6861f39a --- /dev/null +++ b/currency_rate_update_BI/hooks.py @@ -0,0 +1,57 @@ +# Copyright 2026 PT Solusi Aglis Indonesia (http://solusiaglis.co.id) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import logging + +_logger = logging.getLogger(__name__) + + +def post_init_hook(cr, registry): + """Remove stale rate records for all companies' base currencies. + + When a company switches its base currency (e.g. from USD to IDR), + historical rate records for the old foreign currency remain in + res_currency_rate. Odoo's company_rate formula divides the stored + rate by the *last* rate found for the company's current base currency: + + company_rate = rate / last_base_currency_rate + + If last_base_currency_rate != 1.0 the displayed "Unit per IDR" / + "IDR per Unit" columns become wildly incorrect, even though the raw + Technical Rate is correct. + + This hook detects and removes any such stale records so the module + works correctly on both fresh and migrated databases. + """ + cr.execute( + """ + SELECT rc.id, rc.name AS currency_name, COUNT(rcr.id) AS rate_count + FROM res_company rco + JOIN res_currency rc ON rc.id = rco.currency_id + LEFT JOIN res_currency_rate rcr ON rcr.currency_id = rc.id + GROUP BY rc.id, rc.name + HAVING COUNT(rcr.id) > 0 + """ + ) + rows = cr.fetchall() + if not rows: + return + + for currency_id, currency_name, rate_count in rows: + _logger.warning( + "currency_rate_update_BI: Found %d stale rate record(s) for base " + "currency '%s'. These cause incorrect company_rate display. " + "Removing them now.", + rate_count, + currency_name, + ) + cr.execute( + "DELETE FROM res_currency_rate WHERE currency_id = %s", + (currency_id,), + ) + _logger.info( + "currency_rate_update_BI: Removed %d rate record(s) for base " + "currency '%s'.", + rate_count, + currency_name, + ) diff --git a/currency_rate_update_BI/i18n/currency_rate_update_BI.pot b/currency_rate_update_BI/i18n/currency_rate_update_BI.pot new file mode 100644 index 00000000..0bb1df01 --- /dev/null +++ b/currency_rate_update_BI/i18n/currency_rate_update_BI.pot @@ -0,0 +1,29 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * currency_rate_update_BI +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2026-01-01 00:00+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#. module: currency_rate_update_BI +#: model_terms:res.currency.rate.provider,service:currency_rate_update_BI.service_bi +msgid "Bank Indonesia (Kurs Transaksi)" +msgstr "" + +#. module: currency_rate_update_BI +#: code:addons/currency_rate_update_BI/models/res_currency_rate_provider_BI.py:0 +msgid "Cannot connect to Bank Indonesia webservice.\nPlease check your internet connection.\nURL: %(url)s\nError: %(error)s" +msgstr "" + +#. module: currency_rate_update_BI +#: code:addons/currency_rate_update_BI/models/res_currency_rate_provider_BI.py:0 +msgid "Bank Indonesia provider: Company base currency %(currency)s is not supported. BI supports IDR as base or any of: %(list)s" +msgstr "" diff --git a/currency_rate_update_BI/i18n/id.po b/currency_rate_update_BI/i18n/id.po new file mode 100644 index 00000000..a090bde6 --- /dev/null +++ b/currency_rate_update_BI/i18n/id.po @@ -0,0 +1,31 @@ +# Indonesian translation of currency_rate_update_BI +# Copyright (C) 2026 PT Solusi Aglis Indonesia +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2026-01-01 00:00+0700\n" +"Last-Translator: PT Solusi Aglis Indonesia \n" +"Language-Team: Indonesian\n" +"Language: id\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: currency_rate_update_BI +#: model_terms:res.currency.rate.provider,service:currency_rate_update_BI.service_bi +msgid "Bank Indonesia (Kurs Transaksi)" +msgstr "Bank Indonesia (Kurs Transaksi)" + +#. module: currency_rate_update_BI +#: code:addons/currency_rate_update_BI/models/res_currency_rate_provider_BI.py:0 +msgid "Cannot connect to Bank Indonesia webservice.\nPlease check your internet connection.\nURL: %(url)s\nError: %(error)s" +msgstr "Tidak dapat terhubung ke webservice Bank Indonesia.\nMohon periksa koneksi internet Anda.\nURL: %(url)s\nError: %(error)s" + +#. module: currency_rate_update_BI +#: code:addons/currency_rate_update_BI/models/res_currency_rate_provider_BI.py:0 +msgid "Bank Indonesia provider: Company base currency %(currency)s is not supported. BI supports IDR as base or any of: %(list)s" +msgstr "Provider Bank Indonesia: Mata uang dasar perusahaan %(currency)s tidak didukung. BI mendukung IDR sebagai dasar atau salah satu dari: %(list)s" diff --git a/currency_rate_update_BI/models/__init__.py b/currency_rate_update_BI/models/__init__.py new file mode 100644 index 00000000..df585f56 --- /dev/null +++ b/currency_rate_update_BI/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import res_currency_rate_provider_BI diff --git a/currency_rate_update_BI/models/res_currency_rate_provider_BI.py b/currency_rate_update_BI/models/res_currency_rate_provider_BI.py new file mode 100644 index 00000000..2dbb3e47 --- /dev/null +++ b/currency_rate_update_BI/models/res_currency_rate_provider_BI.py @@ -0,0 +1,308 @@ +# Copyright 2026 PT Solusi Aglis Indonesia (http://solusiaglis.co.id) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import logging +from urllib.error import URLError +from urllib.request import Request, urlopen +from xml.etree import ElementTree + +from odoo import _, fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + +# Bank Indonesia SOAP Webservice (HTTP GET style) +# Source: https://www.bi.go.id/biwebservice/wskursbi.asmx +BI_WEBSERVICE_URL = ( + "https://www.bi.go.id/biwebservice/wskursbi.asmx/getSubKursLokal3" + "?mts={currency}&startdate={date_from}&enddate={date_to}" +) + +# BI server requires browser-like headers, otherwise returns empty response +BI_REQUEST_HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" + ), + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Language": "id-ID,id;q=0.9,en;q=0.8", +} + +# Currencies published by Bank Indonesia in Kurs Transaksi BI +# Source: https://www.bi.go.id/en/statistik/informasi-kurs/transaksi-bi/ +BI_SUPPORTED_CURRENCIES = [ + "USD", # US Dollar + "EUR", # Euro + "GBP", # British Pound + "JPY", # Japanese Yen + "SGD", # Singapore Dollar + "AUD", # Australian Dollar + "BND", # Brunei Dollar + "CAD", # Canadian Dollar + "CHF", # Swiss Franc + "CNY", # Chinese Yuan + "DKK", # Danish Krone + "HKD", # Hong Kong Dollar + "KRW", # South Korean Won + "MYR", # Malaysian Ringgit + "NOK", # Norwegian Krone + "NZD", # New Zealand Dollar + "SAR", # Saudi Riyal + "SEK", # Swedish Krona + "THB", # Thai Baht +] + + +class ResCurrencyRateProviderBI(models.Model): + _inherit = "res.currency.rate.provider" + + service = fields.Selection( + selection_add=[("BI", "Bank Indonesia (Kurs Transaksi)")], + ondelete={"BI": "set default"}, + ) + + def _get_supported_currencies(self): + self.ensure_one() + if self.service != "BI": + return super()._get_supported_currencies() + return BI_SUPPORTED_CURRENCIES + + def _obtain_rates(self, base_currency, currencies, date_from, date_to): + self.ensure_one() + if self.service != "BI": + return super()._obtain_rates(base_currency, currencies, date_from, date_to) + + # Determine which currencies to fetch from BI + fetch_currencies = self._get_currencies_to_fetch(base_currency, currencies) + if not fetch_currencies: + return {} + + # Fetch rates from BI webservice + rates_by_date = self._fetch_bi_rates(fetch_currencies, date_from, date_to) + if not rates_by_date: + return {} + + # Convert to Odoo format based on base currency + return self._convert_bi_rates_to_odoo(rates_by_date, base_currency) + + def _get_currencies_to_fetch(self, base_currency, currencies): + """Determine which currencies need to be fetched from BI.""" + fetch_currencies = [c for c in currencies if c in BI_SUPPORTED_CURRENCIES] + # If company base is not IDR but is in BI's list, we need it for cross-rate calc + if base_currency in BI_SUPPORTED_CURRENCIES and base_currency != "IDR": + if base_currency not in fetch_currencies: + fetch_currencies.append(base_currency) + return fetch_currencies + + def _fetch_bi_rates(self, fetch_currencies, date_from, date_to): + """Fetch exchange rates from BI webservice for given currencies.""" + rates_by_date = {} + for currency in fetch_currencies: + url = BI_WEBSERVICE_URL.format( + currency=currency, + date_from=date_from.strftime("%Y-%m-%d"), + date_to=date_to.strftime("%Y-%m-%d"), + ) + try: + req = Request(url, headers=BI_REQUEST_HEADERS) + with urlopen(req, timeout=30) as response: + xml_data = response.read() + currency_rates = self._parse_bi_response(xml_data, currency) + for date_str, rate_value in currency_rates.items(): + if date_str not in rates_by_date: + rates_by_date[date_str] = {} + rates_by_date[date_str][currency] = rate_value + except URLError as e: + _logger.warning( + "Currency Rate Provider BI: Failed to fetch %s rates from BI: %s", + currency, + str(e), + ) + raise UserError( + _( + "Cannot connect to Bank Indonesia webservice.\n" + "Please check your internet connection.\n" + "URL: %(url)s\nError: %(error)s" + ) + % {"url": url, "error": str(e)} + ) from e + return rates_by_date + + def _convert_bi_rates_to_odoo(self, rates_by_date, base_currency): + """Convert BI rates to Odoo format based on base currency. + + BI publishes: X IDR per 1 foreign currency (e.g. 16826 IDR/USD) + Odoo _process_rate expects: {"inverted": X} where X = IDR per 1 foreign + _process_rate will compute: direct = 1/X (foreign per 1 IDR, used as rate) + """ + if base_currency == "IDR": + return self._convert_idr_base_rates(rates_by_date) + elif base_currency in BI_SUPPORTED_CURRENCIES: + return self._convert_foreign_base_rates(rates_by_date, base_currency) + else: + raise UserError( + _( + "Bank Indonesia provider: Company base currency %(currency)s " + "is not supported. BI supports IDR as base or any of: %(list)s" + ) + % { + "currency": base_currency, + "list": ", ".join(BI_SUPPORTED_CURRENCIES), + } + ) + + def _convert_idr_base_rates(self, rates_by_date): + """Convert rates for IDR base currency.""" + content = {} + for date_str, rates in rates_by_date.items(): + content[date_str] = {} + for curr, rate_idr in rates.items(): + # rate_idr = IDR per 1 unit of `curr` (e.g. 16826 for USD) + # Odoo stores "foreign per 1 base" => rate_usd = 1/16826 + # We pass {"inverted": 16826} so _process_rate computes 1/16826 + content[date_str][curr] = {"inverted": rate_idr} + return content + + def _convert_foreign_base_rates(self, rates_by_date, base_currency): + """Convert rates for foreign base currency using cross-rates.""" + content = {} + for date_str, rates in rates_by_date.items(): + if base_currency not in rates: + # Skip dates where base currency rate is missing + continue + base_idr = rates[base_currency] # IDR per 1 base currency + content[date_str] = {} + for curr, rate_idr in rates.items(): + if curr == base_currency: + continue + # Cross rate: foreign per 1 base = (IDR/base) / (IDR/foreign) + # = rate_idr_base / rate_idr_foreign + # e.g. EUR/USD = (IDR/USD) / (IDR/EUR) = 16826 / 18500 ≈ 0.9095 + cross_rate = base_idr / rate_idr + content[date_str][curr] = cross_rate + return content + + def _parse_bi_response(self, xml_data, expected_currency): + """Parse BI SOAP DataSet XML response (diffgram format). + + BI's wsKursBI (getSubKursLokal3) returns a Microsoft ADO.NET DataSet + XML in diffgram format. Actual structure: + + + ... + + + + 962545 + 1.00 + 16741.87 + 16910.13 + 2026-02-06T00:00:00+07:00 + USD +
+ ... +
+
+
+ + Notes: + - No kurs_tengah; rate = (beli + jual) / 2 + - nil_subkurslokal is the nominal unit (1 for USD, may be 100 for JPY) + - mts_subkurslokal has trailing spaces + - Rate stored = (beli + jual) / 2 / nil_subkurslokal + + Returns dict: {date_str: idr_per_1_foreign_unit} + """ + result = {} + try: + root = ElementTree.fromstring(xml_data) + except ElementTree.ParseError as e: + _logger.error("BI: Failed to parse XML response: %s", str(e)) + return result + + # Strip all XML namespaces for simpler XPath queries + for elem in root.iter(): + if "}" in elem.tag: + elem.tag = elem.tag.split("}", 1)[1] + # Also strip namespaced attributes (e.g. diffgr:id) + elem.attrib = { + k.split("}", 1)[1] if "}" in k else k: v for k, v in elem.attrib.items() + } + + # Data lives inside diffgram > NewDataSet > Table + for table in root.findall(".//NewDataSet/Table"): + date_str = self._get_bi_date(table) + if not date_str: + continue + + rate_value = self._get_bi_rate(table) + if rate_value is None: + continue + + result[date_str] = rate_value + + if not result: + _logger.warning( + "BI: No exchange rate data found for %s in XML response. " + "The webservice may not have data for the requested date range, " + "or the XML structure may have changed.", + expected_currency, + ) + return result + + def _get_bi_date(self, table_element): + """Extract and normalize date from a BI Table element. + + BI returns dates as ISO 8601 with timezone: + e.g. "2026-02-06T00:00:00+07:00" + + Returns date string "YYYY-MM-DD" or None if not found. + """ + el = table_element.find("tgl_subkurslokal") + if el is not None and el.text: + return el.text.strip().split("T")[0] + return None + + def _get_bi_rate(self, table_element): + """Extract exchange rate (IDR per 1 foreign currency unit) from Table. + + BI Kurs Transaksi publishes beli (buy) and jual (sell) rates only. + Middle rate = (beli + jual) / 2. + + nil_subkurslokal is the nominal unit. For most currencies it is 1, + but some currencies (e.g. JPY, KRW) may use 100 as the unit, meaning + the listed rate is per 100 units. We divide by nil to normalize to + rate per 1 unit. + + Returns float IDR-per-1-foreign-unit, or None on error. + """ + beli_el = table_element.find("beli_subkurslokal") + jual_el = table_element.find("jual_subkurslokal") + nil_el = table_element.find("nil_subkurslokal") + + if not ( + beli_el is not None + and beli_el.text + and jual_el is not None + and jual_el.text + ): + _logger.warning( + "BI: Could not extract rate from Table element. " "Available tags: %s", + [child.tag for child in table_element], + ) + return None + + try: + beli = float(beli_el.text.strip().replace(",", "")) + jual = float(jual_el.text.strip().replace(",", "")) + nil = ( + float(nil_el.text.strip()) + if nil_el is not None and nil_el.text + else 1.0 + ) + if nil <= 0: + nil = 1.0 + return (beli + jual) / 2 / nil + except (ValueError, ZeroDivisionError): + return None diff --git a/currency_rate_update_BI/readme/CONFIGURE.rst b/currency_rate_update_BI/readme/CONFIGURE.rst new file mode 100644 index 00000000..5472f861 --- /dev/null +++ b/currency_rate_update_BI/readme/CONFIGURE.rst @@ -0,0 +1,14 @@ +To configure Bank Indonesia currency rates provider: + +#. Go to *Invoicing > Configuration > Settings* +#. Ensure *Automatic Currency Rates (OCA)* is checked +#. Go to *Invoicing > Configuration > Currency Rates Providers* +#. Create a new provider with *Provider: Bank Indonesia (Kurs Transaksi)* +#. Select the currencies you want to update (USD, EUR, SGD, etc.) +#. Configure the update schedule (e.g., daily, weekly) +#. Save the provider + +.. note:: + Your company's base currency should be **IDR (Indonesian Rupiah)** + or one of the supported foreign currencies if you need cross-rate + calculations. diff --git a/currency_rate_update_BI/readme/DESCRIPTION.rst b/currency_rate_update_BI/readme/DESCRIPTION.rst new file mode 100644 index 00000000..26b9efdf --- /dev/null +++ b/currency_rate_update_BI/readme/DESCRIPTION.rst @@ -0,0 +1,57 @@ +Adds **Bank Indonesia (BI)** as a currency rates provider for the +``currency_rate_update`` module. Exchange rates are fetched from +Bank Indonesia's official SOAP webservice (wsKursBI) using the +``getSubKursLokal3`` method. + +**Data Source:** Bank Indonesia — https://www.bi.go.id + +**Rate Type:** Kurs Transaksi BI (Transaction Rates — middle rate / kurs tengah) + +**Update Frequency:** Daily on business days (Mon–Fri, excluding Indonesian national holidays) + +**Supported Currencies (vs IDR):** + ++-----+----------------------------+ +| USD | United States Dollar | ++-----+----------------------------+ +| EUR | Euro | ++-----+----------------------------+ +| GBP | British Pound | ++-----+----------------------------+ +| JPY | Japanese Yen | ++-----+----------------------------+ +| SGD | Singapore Dollar | ++-----+----------------------------+ +| AUD | Australian Dollar | ++-----+----------------------------+ +| BND | Brunei Dollar | ++-----+----------------------------+ +| CAD | Canadian Dollar | ++-----+----------------------------+ +| CHF | Swiss Franc | ++-----+----------------------------+ +| CNY | Chinese Yuan | ++-----+----------------------------+ +| DKK | Danish Krone | ++-----+----------------------------+ +| HKD | Hong Kong Dollar | ++-----+----------------------------+ +| KRW | South Korean Won | ++-----+----------------------------+ +| MYR | Malaysian Ringgit | ++-----+----------------------------+ +| NOK | Norwegian Krone | ++-----+----------------------------+ +| NZD | New Zealand Dollar | ++-----+----------------------------+ +| SAR | Saudi Riyal | ++-----+----------------------------+ +| SEK | Swedish Krona | ++-----+----------------------------+ +| THB | Thai Baht | ++-----+----------------------------+ + +**Note:** This module is designed primarily for Indonesian companies +using **IDR (Indonesian Rupiah)** as their base currency. Cross-rate +calculations (e.g. USD base company wanting EUR rates) are supported +as long as the base currency is in the list above. diff --git a/currency_rate_update_BI/readme/USAGE.rst b/currency_rate_update_BI/readme/USAGE.rst new file mode 100644 index 00000000..15c15967 --- /dev/null +++ b/currency_rate_update_BI/readme/USAGE.rst @@ -0,0 +1,18 @@ +After configuration, currency rates are automatically updated based on +your schedule. To manually update rates: + +#. Go to *Invoicing > Configuration > Currency Rates Providers* +#. Select the Bank Indonesia provider +#. Click *Action > Update Rates Wizard* +#. Set the date range (From/To dates) +#. Click *Update* to fetch rates from Bank Indonesia + +To view the fetched rates: + +#. Go to *Invoicing > Configuration > Currencies* +#. Select a currency (e.g., USD) +#. Open the *Rates* tab to see historical exchange rates + +The rates are fetched from Bank Indonesia's official webservice at +https://www.bi.go.id/biwebservice/wskursbi.asmx and represent the +middle rate (average of buy/sell rates) for each currency pair. diff --git a/currency_rate_update_BI/static/description/icon.png b/currency_rate_update_BI/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5617ecd70d7b80ddf2038dd3e410cee2c7ed4f55 GIT binary patch literal 83138 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
*;LTcXy&#+E_&w>m;`*faQRsHHZcVyqA6tNYLIS z$0#=VWz3M%mT@3u*3Qfb#YF7kyyElVccMEq6x-b(-lOI9&yWUhL*LQg0aK?*QR=3Y z4hEo7csY}8yZs-M4Rh9%=ilG-^0cbDDF2n zg;714YA2Rvyg2NJ&>K}{OvIrckmQGRj{UwSvNA0 z^}O4kv*hrnUc}I#Q1Yaw1WiT3>VlnfRUFTP$$;erK~p>G#?mCo_!53umm|`KG=wLh z^8u*QcSMvlYIXUJbi|?PIx&aqe<1Kg$!#&f;tnTqkA#2WS)Ep9&@;$FQ19Pl-H_eV z!WjVu_8eb1ka{qZQI;_w8a+*0)qQ_TNmk)5-k32eMF#rCUELE(Uo3xOeJ2=heG{K56BO@3amN@*};CRaJ>*-P(iPG$r zGV0$JD7wLoAg9AT^lrx~)bAa^q_f`x!0A%oCMBxUA@jb{zpbF~7D75?O&bWoe023z zF`d-S=75b?qRo5|aL#246Xj-+^!x!qD*U+#J=NEt)d;^ z|0R$ShM6hgrW`D0f=w^W&15&`dZyIi-z%w39M&d4oWO+{AlR*%_+GwkgX9f`OMAqS z4PIdFoZIw~QeA8O`6@|9J9MQ2wZlZQ{gyzs75Cf%-5IGC)wbc?5-P))%tT}%eab7x zmT2_v%JuVt#;6dV4?f!DEzb39=+6zQkFOz_?k<%*+WbF0M85Y0a-zeIj*L*znwKY= zG?i#UJU2-eO0eQPAa;62YLWuOf;l@G6Ede*7(oE=9e1B?o)PRfZUdX9UWq2|n)GjP z%pGl8(gb``xe)UvyWx$uY^15iJnX6lUvAvvgO`hV^WP7qkFf*94ld45RpJGmv|9;yEgI54H;S9Z3f@!(B0sW8GG! zFSS~07`NHcDj`2ZUJhWi5av8PIZI2&e~9lyM;EU8pMJ=hAVDzGMR+04`ox2v=}FEX z(&|~jY%j@oL=NmZTTtRYAn3i7FJR&4T`f>ezg``U2ETl&#t;8%q|Oy=|I88jo95cQtSU#JhR#eC5ZsUbW0RK&%s(#$5bJSn093pO+$u0(NTo0&K{nrVc8aaFQWj@hkq47ZvbKp!H#vpbp~ezkx~;!4U`BJY^O@@ zh9s??#C^I?t%qo?D(*&)xH4`j)e3x5_JFVG_@6WUHlLM-@eKA1*Py(b5gOWw-&0fY z@xJ8GL!JM4OyIs2O{qN<>{lm8q76F8cdLDi&iYkH$)Y|I#SDeMq^myI2s$8~XGEXJ z4*5qM>xU#?0&$2ka(m5C;kux#8#%OOv(RioyZ-YZZy}S#h!5-cFK3HtiMP%emH81D z2x~xjM3&B$RcQuBe5rmxUoH<3ndLicE+9fW1Lis6&w|Y;31vx-Lz?pGVb*!dbSK6S zpK_)+&}DhTheN=u1k@-)IXSWtdu+s`E4L2)GMKg!5ES9tQ6E??ExDugh5rxG%S1YD z9$Kcy!x3d3pO%O_hFp8Vs&m=Px4;s_#7HHHRZvi164pa9RH@8UQ;MWk0Pj~&DMiN588BGJ-)G7jDHyR`$ZzXR+yo=Z9QJSTjqx`=K zL>fJprQJqa#0pv|)|n+T1==Z!yJpPUBFS<2tkM5>_^W6p$6u%xFjFi1Z{q(S|K9@t f?-uY0zk3zP`mxCnTRm|Dczu)=)a5H>A;JF#Cv~R} literal 0 HcmV?d00001 diff --git a/currency_rate_update_BI/static/description/index.html b/currency_rate_update_BI/static/description/index.html new file mode 100644 index 00000000..164c9688 --- /dev/null +++ b/currency_rate_update_BI/static/description/index.html @@ -0,0 +1,533 @@ + + + + + +Currency Rate Update: Bank Indonesia + + + +
+

Currency Rate Update: Bank Indonesia

+ + +

Beta License: AGPL-3 OCA/currency Translate me on Weblate Try me on Runboat

+

Adds Bank Indonesia (BI) as a currency rates provider for the +currency_rate_update module. Exchange rates are fetched from +Bank Indonesia’s official SOAP webservice (wsKursBI) using the +getSubKursLokal3 method.

+

Data Source: Bank Indonesia — https://www.bi.go.id

+

Rate Type: Kurs Transaksi BI (Transaction Rates — middle rate / kurs tengah)

+

Update Frequency: Daily on business days (Mon–Fri, excluding Indonesian national holidays)

+

Supported Currencies (vs IDR):

+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
USDUnited States Dollar
EUREuro
GBPBritish Pound
JPYJapanese Yen
SGDSingapore Dollar
AUDAustralian Dollar
BNDBrunei Dollar
CADCanadian Dollar
CHFSwiss Franc
CNYChinese Yuan
DKKDanish Krone
HKDHong Kong Dollar
KRWSouth Korean Won
MYRMalaysian Ringgit
NOKNorwegian Krone
NZDNew Zealand Dollar
SARSaudi Riyal
SEKSwedish Krona
THBThai Baht
+

Note: This module is designed primarily for Indonesian companies +using IDR (Indonesian Rupiah) as their base currency. Cross-rate +calculations (e.g. USD base company wanting EUR rates) are supported +as long as the base currency is in the list above.

+

Table of contents

+ +
+

Configuration

+

To configure Bank Indonesia currency rates provider:

+
    +
  1. Go to Invoicing > Configuration > Settings
  2. +
  3. Ensure Automatic Currency Rates (OCA) is checked
  4. +
  5. Go to Invoicing > Configuration > Currency Rates Providers
  6. +
  7. Create a new provider with Provider: Bank Indonesia (Kurs Transaksi)
  8. +
  9. Select the currencies you want to update (USD, EUR, SGD, etc.)
  10. +
  11. Configure the update schedule (e.g., daily, weekly)
  12. +
  13. Save the provider
  14. +
+
+

Note

+

Your company’s base currency should be IDR (Indonesian Rupiah) +or one of the supported foreign currencies if you need cross-rate +calculations.

+
+
+
+

Usage

+

After configuration, currency rates are automatically updated based on +your schedule. To manually update rates:

+
    +
  1. Go to Invoicing > Configuration > Currency Rates Providers
  2. +
  3. Select the Bank Indonesia provider
  4. +
  5. Click Action > Update Rates Wizard
  6. +
  7. Set the date range (From/To dates)
  8. +
  9. Click Update to fetch rates from Bank Indonesia
  10. +
+

To view the fetched rates:

+
    +
  1. Go to Invoicing > Configuration > Currencies
  2. +
  3. Select a currency (e.g., USD)
  4. +
  5. Open the Rates tab to see historical exchange rates
  6. +
+

The rates are fetched from Bank Indonesia’s official webservice at +https://www.bi.go.id/biwebservice/wskursbi.asmx and represent the +middle rate (average of buy/sell rates) for each currency pair.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • PT Solusi Aglis Indonesia
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

hitrosol

+

This module is part of the OCA/currency project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/currency_rate_update_BI/tests/__init__.py b/currency_rate_update_BI/tests/__init__.py new file mode 100644 index 00000000..5bbf575f --- /dev/null +++ b/currency_rate_update_BI/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import test_currency_rate_update_BI diff --git a/currency_rate_update_BI/tests/test_currency_rate_update_BI.py b/currency_rate_update_BI/tests/test_currency_rate_update_BI.py new file mode 100644 index 00000000..2bace189 --- /dev/null +++ b/currency_rate_update_BI/tests/test_currency_rate_update_BI.py @@ -0,0 +1,512 @@ +# Copyright 2026 PT Solusi Aglis Indonesia (http://solusiaglis.co.id) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from io import BytesIO +from unittest.mock import patch + +from odoo import fields +from odoo.tests import common + +# Real XML response format from Bank Indonesia wsKursBI getSubKursLokal3 +# Confirmed from live BI webservice (diffgram format, ADO.NET DataSet) +SAMPLE_BI_XML_USD = b""" + + + + + + + + + + + + + + + + + + + + + + + + + + 962545 + 44 + 1.00 + 16741.87 + 16910.13 + 2026-02-06T00:00:00+07:00 + USD +
+ + 962310 + 44 + 1.00 + 16691.13 + 16858.88 + 2026-02-05T00:00:00+07:00 + USD +
+ + 962075 + 44 + 1.00 + 16693.12 + 16860.88 + 2026-02-04T00:00:00+07:00 + USD +
+ + 961840 + 44 + 1.00 + 16716.00 + 16884.00 + 2026-02-03T00:00:00+07:00 + USD +
+
+
+
""" + +SAMPLE_BI_XML_EUR = b""" + + + + + + + + + + + + + + + + + + + + + + + + + + 962550 + 44 + 1.00 + 17421.00 + 17605.00 + 2026-02-06T00:00:00+07:00 + EUR +
+ + 962315 + 44 + 1.00 + 17380.00 + 17560.00 + 2026-02-05T00:00:00+07:00 + EUR +
+ + 962080 + 44 + 1.00 + 17390.00 + 17570.00 + 2026-02-04T00:00:00+07:00 + EUR +
+ + 961845 + 44 + 1.00 + 17400.00 + 17580.00 + 2026-02-03T00:00:00+07:00 + EUR +
+
+
+
""" + +# Sample with nil_subkurslokal=100 (e.g. JPY - quoted per 100 units) +SAMPLE_BI_XML_JPY_NIL100 = b""" + + + + + + + + + + + + + + + + + 100.00 + 10700.00 + 10900.00 + 2026-02-06T00:00:00+07:00 + JPY +
+
+
+
""" + + +def _mock_urlopen(req, *args, **kwargs): + """Mock urlopen returning sample BI XML based on currency code in URL.""" + url_str = req.full_url if hasattr(req, "full_url") else str(req) + if "mts=USD" in url_str: + return BytesIO(SAMPLE_BI_XML_USD) + elif "mts=EUR" in url_str: + return BytesIO(SAMPLE_BI_XML_EUR) + elif "mts=JPY" in url_str: + return BytesIO(SAMPLE_BI_XML_JPY_NIL100) + return BytesIO( + b'' + b'' + b'' + ) + + +class TestResCurrencyRateProviderBI(common.TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.Company = cls.env["res.company"] + cls.CurrencyRate = cls.env["res.currency.rate"] + cls.CurrencyRateProvider = cls.env["res.currency.rate.provider"] + + cls.today = fields.Date.today() + cls.idr_currency = cls.env.ref("base.IDR") + cls.usd_currency = cls.env.ref("base.USD") + cls.eur_currency = cls.env.ref("base.EUR") + cls.jpy_currency = cls.env.ref("base.JPY") + + cls.company = cls.Company.create( + {"name": "Test PT Indonesia", "currency_id": cls.idr_currency.id} + ) + cls.env.user.company_ids += cls.company + cls.env.company = cls.company + + cls.bi_provider = cls.CurrencyRateProvider.create( + { + "service": "BI", + "currency_ids": [ + (4, cls.usd_currency.id), + (4, cls.eur_currency.id), + ], + } + ) + cls.CurrencyRate.search([]).unlink() + + @patch( + "odoo.addons.currency_rate_update_BI.models" + ".res_currency_rate_provider_BI.urlopen", + side_effect=_mock_urlopen, + ) + def test_scheduled_update(self, mock_urlopen): + """Scheduled cron creates rates for USD and EUR.""" + self.bi_provider._scheduled_update() + rates = self.CurrencyRate.search([("provider_id", "=", self.bi_provider.id)]) + currencies = rates.mapped("currency_id") + self.assertIn(self.usd_currency, currencies) + self.assertIn(self.eur_currency, currencies) + + @patch( + "odoo.addons.currency_rate_update_BI.models" + ".res_currency_rate_provider_BI.urlopen", + side_effect=_mock_urlopen, + ) + def test_wizard_update(self, mock_urlopen): + """Wizard update creates rates for full date range (USD×4 + EUR×4 = 8).""" + from datetime import date + + wizard = ( + self.env["res.currency.rate.update.wizard"] + .with_context(default_provider_ids=[(6, False, self.bi_provider.ids)]) + .create( + { + "date_from": date(2026, 2, 3), + "date_to": date(2026, 2, 6), + } + ) + ) + wizard.action_update() + rates = self.CurrencyRate.search([("provider_id", "=", self.bi_provider.id)]) + self.assertEqual(len(rates), 8) + + @patch( + "odoo.addons.currency_rate_update_BI.models" + ".res_currency_rate_provider_BI.urlopen", + side_effect=_mock_urlopen, + ) + def test_rate_value_idr_base(self, mock_urlopen): + """Rate for USD on 2026-02-06: (16741.87+16910.13)/2 = 16826.0 IDR/USD. + Stored in Odoo as 1/16826.0 (foreign per 1 IDR base). + """ + from datetime import date + + wizard = ( + self.env["res.currency.rate.update.wizard"] + .with_context(default_provider_ids=[(6, False, self.bi_provider.ids)]) + .create( + { + "date_from": date(2026, 2, 6), + "date_to": date(2026, 2, 6), + } + ) + ) + wizard.action_update() + + usd_rate = self.CurrencyRate.search( + [ + ("provider_id", "=", self.bi_provider.id), + ("currency_id", "=", self.usd_currency.id), + ("name", "=", date(2026, 2, 6)), + ], + limit=1, + ) + self.assertTrue(usd_rate, "USD rate for 2026-02-06 should exist") + # (16741.87 + 16910.13) / 2 = 16826.0 IDR/USD → stored as 1/16826 + expected_rate = 1 / 16826.0 + self.assertAlmostEqual(usd_rate.rate, expected_rate, places=10) + + def test_parse_bi_response_usd(self): + """Parse real-format XML: 4 USD rows returned correctly.""" + rates = self.bi_provider._parse_bi_response(SAMPLE_BI_XML_USD, "USD") + self.assertEqual(len(rates), 4) + self.assertIn("2026-02-03", rates) + self.assertIn("2026-02-04", rates) + self.assertIn("2026-02-05", rates) + self.assertIn("2026-02-06", rates) + # 2026-02-06: (16741.87 + 16910.13) / 2 / 1 = 16826.0 + self.assertAlmostEqual(rates["2026-02-06"], 16826.0) + # 2026-02-03: (16716.00 + 16884.00) / 2 / 1 = 16800.0 + self.assertAlmostEqual(rates["2026-02-03"], 16800.0) + + def test_parse_bi_response_nil100(self): + """nil_subkurslokal=100: rate normalized to per-1-unit. + JPY: (10700 + 10900) / 2 / 100 = 108.0 IDR/JPY + """ + rates = self.bi_provider._parse_bi_response(SAMPLE_BI_XML_JPY_NIL100, "JPY") + self.assertEqual(len(rates), 1) + self.assertAlmostEqual(rates["2026-02-06"], 108.0) + + def test_parse_bi_response_empty(self): + """Empty DataSet returns empty dict without raising.""" + empty_xml = ( + b'' + b'' + b'' + b'' + b"" + ) + rates = self.bi_provider._parse_bi_response(empty_xml, "USD") + self.assertEqual(rates, {}) + + def test_supported_currencies(self): + """BI provider returns correct supported currencies list.""" + currencies = self.bi_provider._get_supported_currencies() + self.assertIn("USD", currencies) + self.assertIn("EUR", currencies) + self.assertIn("JPY", currencies) + self.assertIn("SGD", currencies) + self.assertIn("MYR", currencies) + self.assertNotIn("IDR", currencies) + + @patch( + "odoo.addons.currency_rate_update_BI.models" + ".res_currency_rate_provider_BI.urlopen", + side_effect=_mock_urlopen, + ) + def test_foreign_base_currency_cross_rate(self, mock_urlopen): + """Test cross-rate calculation for foreign base currency (USD base).""" + from datetime import date + + # Create USD-based company + usd_company = self.Company.create( + {"name": "Test US Company", "currency_id": self.usd_currency.id} + ) + bi_provider_usd = self.CurrencyRateProvider.with_company(usd_company).create( + { + "service": "BI", + "currency_ids": [(4, self.eur_currency.id)], + "company_id": usd_company.id, + } + ) + + wizard = ( + self.env["res.currency.rate.update.wizard"] + .with_context(default_provider_ids=[(6, False, bi_provider_usd.ids)]) + .with_company(usd_company) + .create( + { + "date_from": date(2026, 2, 6), + "date_to": date(2026, 2, 6), + } + ) + ) + wizard.action_update() + + eur_rate = self.CurrencyRate.search( + [ + ("provider_id", "=", bi_provider_usd.id), + ("currency_id", "=", self.eur_currency.id), + ("name", "=", date(2026, 2, 6)), + ], + limit=1, + ) + self.assertTrue(eur_rate, "EUR rate for USD base should exist") + # Cross rate: EUR/USD = IDR/USD ÷ IDR/EUR = 16826 ÷ 17513 ≈ 0.9608 + # IDR/USD = (16741.87 + 16910.13) / 2 = 16826 + # IDR/EUR = (17421 + 17605) / 2 = 17513 + expected_cross_rate = 16826.0 / 17513.0 + self.assertAlmostEqual(eur_rate.rate, expected_cross_rate, places=4) + + def test_unsupported_base_currency(self): + """Test error when base currency not supported by BI.""" + from datetime import date + + from odoo.exceptions import UserError + + # BRL (Brazilian Real) is not in BI_SUPPORTED_CURRENCIES + brl_currency = self.env.ref("base.BRL") + brl_company = self.Company.create( + {"name": "Test Brazil Company", "currency_id": brl_currency.id} + ) + bi_provider_brl = self.CurrencyRateProvider.with_company(brl_company).create( + { + "service": "BI", + "currency_ids": [(4, self.usd_currency.id)], + "company_id": brl_company.id, + } + ) + + with self.assertRaises(UserError) as cm: + bi_provider_brl._obtain_rates( + "BRL", ["USD"], date(2026, 2, 6), date(2026, 2, 6) + ) + self.assertIn("not supported", str(cm.exception)) + + @patch( + "odoo.addons.currency_rate_update_BI.models" + ".res_currency_rate_provider_BI.urlopen" + ) + def test_connection_error(self, mock_urlopen): + """Test handling of connection error.""" + from datetime import date + from urllib.error import URLError + + from odoo.exceptions import UserError + + mock_urlopen.side_effect = URLError("Connection timeout") + + with self.assertRaises(UserError) as cm: + self.bi_provider._obtain_rates( + "IDR", ["USD"], date(2026, 2, 6), date(2026, 2, 6) + ) + self.assertIn("Cannot connect", str(cm.exception)) + + def test_parse_missing_fields(self): + """Test parsing XML with missing beli/jual fields.""" + missing_fields_xml = b""" + + + + + 1.00 + 2026-02-06T00:00:00+07:00 + USD +
+
+
+
""" + rates = self.bi_provider._parse_bi_response(missing_fields_xml, "USD") + self.assertEqual(rates, {}) + + def test_parse_invalid_xml(self): + """Test parsing invalid XML returns empty dict.""" + invalid_xml = b"xml" + rates = self.bi_provider._parse_bi_response(invalid_xml, "USD") + self.assertEqual(rates, {}) + + def test_parse_missing_date(self): + """Test parsing XML with missing date field.""" + missing_date_xml = b""" + + + + + 1.00 + 16741.87 + 16910.13 + USD +
+
+
+
""" + rates = self.bi_provider._parse_bi_response(missing_date_xml, "USD") + self.assertEqual(rates, {}) + + def test_get_currencies_to_fetch_idr_base(self): + """Test _get_currencies_to_fetch with IDR base.""" + fetch = self.bi_provider._get_currencies_to_fetch("IDR", ["USD", "EUR", "GBP"]) + self.assertIn("USD", fetch) + self.assertIn("EUR", fetch) + self.assertIn("GBP", fetch) + self.assertNotIn("IDR", fetch) + + def test_get_currencies_to_fetch_foreign_base(self): + """Test _get_currencies_to_fetch with USD base adds USD to fetch list.""" + fetch = self.bi_provider._get_currencies_to_fetch("USD", ["EUR", "GBP"]) + self.assertIn("USD", fetch) # Added for cross-rate calculation + self.assertIn("EUR", fetch) + self.assertIn("GBP", fetch) + + def test_convert_idr_base_rates(self): + """Test _convert_idr_base_rates returns inverted format.""" + rates_by_date = {"2026-02-06": {"USD": 16826.0}} + result = self.bi_provider._convert_idr_base_rates(rates_by_date) + self.assertEqual(result["2026-02-06"]["USD"], {"inverted": 16826.0}) + + def test_convert_foreign_base_rates(self): + """Test _convert_foreign_base_rates calculates cross rates.""" + rates_by_date = {"2026-02-06": {"USD": 16826.0, "EUR": 17513.0}} + result = self.bi_provider._convert_foreign_base_rates(rates_by_date, "USD") + # EUR/USD = 16826 / 17513 + expected = 16826.0 / 17513.0 + self.assertAlmostEqual(result["2026-02-06"]["EUR"], expected, places=4) + + def test_convert_foreign_base_rates_missing_base(self): + """Test _convert_foreign_base_rates skips dates without base currency.""" + rates_by_date = { + "2026-02-06": {"EUR": 17513.0}, # Missing USD + "2026-02-07": {"USD": 16826.0, "EUR": 17513.0}, + } + result = self.bi_provider._convert_foreign_base_rates(rates_by_date, "USD") + self.assertNotIn("2026-02-06", result) # Skipped + self.assertIn("2026-02-07", result) diff --git a/setup/currency_rate_update_BI/odoo/addons/currency_rate_update_BI b/setup/currency_rate_update_BI/odoo/addons/currency_rate_update_BI new file mode 120000 index 00000000..928614a3 --- /dev/null +++ b/setup/currency_rate_update_BI/odoo/addons/currency_rate_update_BI @@ -0,0 +1 @@ +../../../../currency_rate_update_BI \ No newline at end of file diff --git a/setup/currency_rate_update_BI/setup.py b/setup/currency_rate_update_BI/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/currency_rate_update_BI/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 8b8ccdb8371f12ac0716f735b4bacb91e13d525c Mon Sep 17 00:00:00 2001 From: hitrosol Date: Fri, 20 Feb 2026 14:08:42 +0700 Subject: [PATCH 2/2] [IMP] currency_rate_update_BI: Support IDR selection for non-IDR base When company base currency is USD (or any foreign currency supported by BI), users can now select and get IDR exchange rates. --- .../models/res_currency_rate_provider_BI.py | 16 +++- .../tests/test_currency_rate_update_BI.py | 88 +++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/currency_rate_update_BI/models/res_currency_rate_provider_BI.py b/currency_rate_update_BI/models/res_currency_rate_provider_BI.py index 2dbb3e47..0fc197ac 100644 --- a/currency_rate_update_BI/models/res_currency_rate_provider_BI.py +++ b/currency_rate_update_BI/models/res_currency_rate_provider_BI.py @@ -66,7 +66,16 @@ def _get_supported_currencies(self): self.ensure_one() if self.service != "BI": return super()._get_supported_currencies() - return BI_SUPPORTED_CURRENCIES + + # Start with currencies that BI publishes rates for + supported = BI_SUPPORTED_CURRENCIES.copy() + + # If company base is not IDR, add IDR to supported list + # This allows selecting IDR to get its rate when base is foreign (e.g., USD) + if self.company_id.currency_id.name != "IDR": + supported.append("IDR") + + return supported def _obtain_rates(self, base_currency, currencies, date_from, date_to): self.ensure_one() @@ -181,6 +190,11 @@ def _convert_foreign_base_rates(self, rates_by_date, base_currency): # e.g. EUR/USD = (IDR/USD) / (IDR/EUR) = 16826 / 18500 ≈ 0.9095 cross_rate = base_idr / rate_idr content[date_str][curr] = cross_rate + + # Add IDR rate if needed + # When base is USD and BI provides 16826 IDR/USD, + # the rate is simply 16826 (direct rate: IDR per 1 USD) + content[date_str]["IDR"] = base_idr return content def _parse_bi_response(self, xml_data, expected_currency): diff --git a/currency_rate_update_BI/tests/test_currency_rate_update_BI.py b/currency_rate_update_BI/tests/test_currency_rate_update_BI.py index 2bace189..8fb2f9f1 100644 --- a/currency_rate_update_BI/tests/test_currency_rate_update_BI.py +++ b/currency_rate_update_BI/tests/test_currency_rate_update_BI.py @@ -510,3 +510,91 @@ def test_convert_foreign_base_rates_missing_base(self): result = self.bi_provider._convert_foreign_base_rates(rates_by_date, "USD") self.assertNotIn("2026-02-06", result) # Skipped self.assertIn("2026-02-07", result) + + @patch( + "odoo.addons.currency_rate_update_BI.models" + ".res_currency_rate_provider_BI.urlopen", + side_effect=_mock_urlopen, + ) + def test_foreign_base_with_idr_selected(self, mock_urlopen): + """Test that IDR can be selected and has correct rate when base is USD.""" + from datetime import date + + # Create USD-based company + usd_company = self.Company.create( + {"name": "Test US Company IDR", "currency_id": self.usd_currency.id} + ) + + bi_provider_usd = self.CurrencyRateProvider.with_company(usd_company).create( + { + "service": "BI", + "currency_ids": [ + (4, self.idr_currency.id), + (4, self.eur_currency.id), + ], + "company_id": usd_company.id, + } + ) + + wizard = ( + self.env["res.currency.rate.update.wizard"] + .with_context(default_provider_ids=[(6, False, bi_provider_usd.ids)]) + .with_company(usd_company) + .create( + { + "date_from": date(2026, 2, 6), + "date_to": date(2026, 2, 6), + } + ) + ) + wizard.action_update() + + # Check IDR rate was created + idr_rate = self.CurrencyRate.search( + [ + ("provider_id", "=", bi_provider_usd.id), + ("currency_id", "=", self.idr_currency.id), + ("name", "=", date(2026, 2, 6)), + ], + limit=1, + ) + self.assertTrue(idr_rate, "IDR rate for USD base should exist") + + # IDR/USD rate from BI: (16741.87 + 16910.13) / 2 = 16826.0 + expected_rate = 16826.0 + self.assertAlmostEqual(idr_rate.rate, expected_rate, places=1) + + # Also verify EUR cross-rate still works + eur_rate = self.CurrencyRate.search( + [ + ("provider_id", "=", bi_provider_usd.id), + ("currency_id", "=", self.eur_currency.id), + ("name", "=", date(2026, 2, 6)), + ], + limit=1, + ) + self.assertTrue(eur_rate, "EUR rate should also exist") + expected_eur_rate = 16826.0 / 17513.0 + self.assertAlmostEqual(eur_rate.rate, expected_eur_rate, places=4) + + def test_supported_currencies_idr_base(self): + """Test that IDR is NOT in supported currencies when base is IDR.""" + currencies = self.bi_provider._get_supported_currencies() + self.assertNotIn("IDR", currencies) + + def test_supported_currencies_usd_base(self): + """Test that IDR IS in supported currencies when base is USD.""" + usd_company = self.Company.create( + {"name": "Test US Company", "currency_id": self.usd_currency.id} + ) + bi_provider_usd = self.CurrencyRateProvider.with_company(usd_company).create( + { + "service": "BI", + "currency_ids": [], + "company_id": usd_company.id, + } + ) + currencies = bi_provider_usd._get_supported_currencies() + self.assertIn("IDR", currencies) + self.assertIn("USD", currencies) + self.assertIn("EUR", currencies)