Currencies Developer Guide - edmozley/freeitsm GitHub Wiki
How FreeITSM knows about currencies, and how a module that records money should use it. User-facing page: Currencies. Introduced for issue #189.
| 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>| 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. |
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.
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:
IDRis 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.
| 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.
Three choices joined by colons: separators:symbol:decimals, e.g. dot_comma:symbol_after:std → 1.234,56 €.
- separators:
comma_dotdot_commaspace_commaspace_dotapos_dotnone_dotindian - symbol:
symbol_beforesymbol_before_spacesymbol_aftercode_beforecode_after - decimals:
std(the currency's own)02
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.
Money::formatFor($code), first match wins:
- the analyst's
currency_formats[code] - the analyst's
currency_format - the install's
currency_formats[code] - the install's
currency_format -
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.
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'sround(1.005, 2)is 1.01. The JS rounds with the exponent trick (Math.round(Number(v + 'e' + dp))) so both give1.01. -
Multi-byte separators. The space styles use a non-breaking space and the Swiss style
’. Grouping is done from the right withsubstr(), neverstrrev(), 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.
-
Column: the code,
VARCHAR(3)or wider, nullable if a value may have no currency. -
Service:
$f['currency'] = moneyServiceCurrency($get('currency'));- add?? Money::defaultCurrency()if blank should mean the default (Software Licences does this; Contracts and Domains keep null). -
Page:
require_once 'includes/money.php', thenMoney::scriptTag()andmoney.jsafteri18n.js. -
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. -
Display:
Money.format(amount, code); when the record has no currency,Money.number(amount)- never a guessed symbol. -
System → Currencies: add the table to the "not recognised" list in
system/currencies/index.php.
| 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.
- Currencies - the user page
- Date and Time Formats - Developer Guide - the pattern this follows