Repository navigation
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.
| 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
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
-
Architecture
- β³ π§Ή Root folder tidy (URL routing)
- β³ π Database Integrity (FKs & constraints)
- β³ π οΈ Database Verification β Developer Guide
- β³ π§© Service Layer (UI + API share code)
- β³ πͺ€ Code traps (the TRAP: comments)
- β³ π Commenting for the Engineering wiki
- β³ π οΈ Engineering wiki scanner - Developer Guide
- β³ π Service-Layer progress
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- π± Currencies
- π Display order (modules, Watchtower cards) β Developer Guide
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: Projects
- β³ ποΈ Mobile: Files
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π REST API: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ π§βπΌ REST API: Analysts
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ π Attachment size limits and large emails
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- π Projects
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Leases
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- ποΈ Files
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ π’ One board across every company
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)
- What this is
-
π Bugs resolved
- β³ π #188 Transfer lock shown off when unknown
- β³ β #184 Subtask names and Overdue
- β³ π§© #186 Teams: the buttons did nothing
- β³ π Text in a change could run script
- β³ π AI settings changed without the AI permission
- β³ πͺ #178 Deleting an analyst failed
- β³ π« A team with no departments showed 0 tickets
- β³ π Projects: adding a phase did nothing on Save
- β³ π Leavers could still sign in
- β³ πΌοΈ Logo and courses broke on Apache with PHP-FPM
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96