Skip to content

Currencies Developer Guide

Ed Mozley edited this page Oct 17, 2026 · 1 revision

Currencies - Developer Guide

How FreeITSM knows about currencies, and how a module that records money should use it. User-facing page: Currencies. Introduced for issue #189.

The short version

You want to Use
Offer a currency dropdown Money.fill(selectEl, storedCode) (JS) or Money.optionsHtml(code, {blank})
Show an amount Money.format(amount, code) (JS) / Money::format($amount, $code) (PHP)
Show an amount that has no currency Money.number(amount) / Money::number($amount) - separators, no symbol
Check a code in a service moneyServiceCurrency($raw) - returns the upper-cased code or null, throws ServiceError
Know the default currency Money.def() / Money::defaultCurrency()

A page that uses the JS needs both, in this order, after i18n.js:

<?php echo Money::scriptTag(); ?>
<script src="../assets/js/money.js?v=1"></script>

The files

File What it is
includes/currencies.php The registry: code => [English name, symbol, decimals, retired]. Data only.
includes/money.php class Money - reads the registry and the settings, formats, validates, publishes to the browser. Also moneyServiceCurrency().
assets/js/money.js window.Money - the same formatter, the dropdown builder, the format picker.
system/currencies/index.php System β†’ Currencies
system/preferences/index.php The Money section
tests/money.php Registry, code checks, settings cleaners, 30-odd formatting cases. No database.

Rows store the code, not an id

Every currency column (contracts.currency, software_licences.currency, domains.currency, lease_schedules.currency, projects.currency) holds the ISO 4217 code. That was already true, and #189 deliberately kept it:

  • The code is a stable international identifier. A numeric id pointing into a PHP array would break the moment somebody reordered the file, and MySQL could not enforce it.
  • API consumers and exports get GBP, which means something to them.
  • Nothing needed migrating.

The registry

includes/currencies.php lists every current ISO 4217 currency, plus a short list of retired ones (BGN, HRK, ANG, ...). Left out on purpose: fund codes (CLF, USN, ...), precious metals (XAU), testing codes (XTS, XXX).

  • Retired = recognised (Money::isCode() is true, a record holding one still saves and displays) but never offered (Money::isCurrent() is false). When a country retires a currency, flag the row; do not delete it.
  • Decimals are ISO's minor units, with one deliberate exception: IDR is 0, because nobody in Indonesia writes ,00 (the original request).
  • Names are an English fallback. The browser supplies the name in the viewer's language through Intl.DisplayNames, so 150-odd names never go through the translation files.
  • Symbols: the locally recognised one, with a prefix where a symbol is shared ($ USD, A$ AUD, CA$ CAD, Β₯ JPY, CNΒ₯ CNY). Where nothing is widely known, the symbol is the code.

Adding a new currency is one row and a release. No schema change.

Settings

Key Where Value
currency_default system_settings a code
currency_favourites system_settings CSV of codes, at most 5, default first
currency_format system_settings a format key
currency_formats system_settings JSON {code: format key}
currency_format user_preferences a format key, or '' = follow the install
currency_formats user_preferences JSON {code: format key}

No new tables. The system keys are owned by the System module in includes/settings_keys.php (tab currencies) and written through the generic save_system_settings.php; the personal ones through set_user_preference.php. Neither writer validates content, so Money cleans every value as it reads it (cleanFavourites(), cleanFormat(), cleanFormatMap()) and ignores anything it does not recognise. A hand-edited row cannot break a page.

A format key

Three choices joined by colons: separators:symbol:decimals, e.g. dot_comma:symbol_after:std β†’ 1.234,56 €.

  • separators: comma_dot dot_comma space_comma space_dot apos_dot none_dot indian
  • symbol: symbol_before symbol_before_space symbol_after code_before code_after
  • decimals: std (the currency's own) 0 2

Stored as a key, never a pattern string - the same rule as the date formats, for the same reason: PHP and JS render it with the same code and cannot drift.

Which format applies

Money::formatFor($code), first match wins:

  1. the analyst's currency_formats[code]
  2. the analyst's currency_format
  3. the install's currency_formats[code]
  4. the install's currency_format
  5. Money::FORMAT_DEFAULT (comma_dot:symbol_before:std)

An analyst who sets (2) has said "this is how I read money", so it beats the install's per-currency rule. jsPayload() resolves this server-side into all + per, so the browser never re-implements the order.

PHP and JS must agree

Money::format() and Money.format() are the same algorithm, written twice. Change both together. Two traps they both handle:

  • Rounding. Math.round(1.005 * 100) is 100 in JS; PHP's round(1.005, 2) is 1.01. The JS rounds with the exponent trick (Math.round(Number(v + 'e' + dp))) so both give 1.01.
  • Multi-byte separators. The space styles use a non-breaking space and the Swiss style ’. Grouping is done from the right with substr(), never strrev(), which would scramble a multi-byte character.

A symbol ending in a letter (CHF, Rp, kr) gets a non-breaking space before the digits; Β£ and $ do not.

The parity was checked by running 22 cases through both in headless Chrome; tests/money.php holds the PHP side of those cases.

Adding money to a module

  1. Column: the code, VARCHAR(3) or wider, nullable if a value may have no currency.
  2. Service: $f['currency'] = moneyServiceCurrency($get('currency')); - add ?? Money::defaultCurrency() if blank should mean the default (Software Licences does this; Contracts and Domains keep null).
  3. Page: require_once 'includes/money.php', then Money::scriptTag() and money.js after i18n.js.
  4. Dropdown: Money.fill(sel, record ? record.currency || '' : Money.def(), { blank: 'β€”' }). Pass the stored code so a retired or unrecognised one is kept as its own option instead of silently changing.
  5. Display: Money.format(amount, code); when the record has no currency, Money.number(amount) - never a guessed symbol.
  6. System β†’ Currencies: add the table to the "not recognised" list in system/currencies/index.php.

Where it is used

Module Dropdown Display Service check
Contracts contracts/edit.php contracts/view.php includes/services/contracts.php
Software software/licences/index.php same includes/services/software.php
Domains domains/view.php + domains-table.js (inline edit) domains.js, domain-links.js includes/services/domains.php
Leases assets/js/leases.js same includes/leases.php
Projects projects-budget.js (change currency), projects/settings budget, control, intake, settings, portfolio projectValidCurrency(), projects/settings.php
Report packs - blocks_software.php -

Chart axis ticks in Projects (notation: 'compact', e.g. Β£1.2M) still use the browser's Intl.NumberFormat - compact suffixes are language-specific and the registry has no data for them.

See also

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally