Skip to content

Asset Tag Numbering Developer Guide

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

Asset tag numbering β€” Developer Guide

Since 3.1.0 Β· Built by Sandy (@srinivasansanthosh) in PR #164, merged with the changes in the review Β· User side: QR asset labels

How FreeITSM gives a new asset the next tag in sequence (AST-00042), and how every write of a tag β€” typed, generated or edited β€” is kept unique within a company.


1. πŸ“ The files

File Role
includes/services/asset_tags.php AssetTagsService β€” settings, validateFormat(), preview(), generate(), createWithTag(), assign(), withLock()
api/assets/get_asset_tag_settings.php, save_asset_tag_settings.php The settings tab's read and write (Cap::ASSETS_TAGS)
api/assets/asset_tag_preview.php Live preview β€” writes nothing
api/assets/save_asset_tag.php The tag box on an asset β€” now AssetTagsService::assign()
asset_tag_counters The counters (counter_key PK, registered in db_verify.php's $primaryKeys)
tests/asset-tag-numbering.php 29 checks

2. 🧩 The shape is ticket numbering's, on purpose

3.0.0 shipped ticket numbering you choose the shape of. Asset tags use the same model rather than a second one:

Setting Notes
On/off asset_tag_autogen_enabled Per company. Off by default β€” an upgrade changes nothing.
Format asset_tag_format Per company. Default AST-{#####}. Rendered by TicketNumbering::render() itself, so the tokens and their rules are identical: {###} is a minimum width, never a limit; {COMPANY} is the company's ticket code.
Start asset_tag_start Per company. Used the first time a counter is claimed.
Scope asset_tag_scope Install-wide. per_company (default β€” tags only have to be unique within a company) or global.

Sandy's original had five settings β€” prefix, suffix, padding, initial number, enabled. A format string is those in one line, and the day somebody asks for {COMPANY}-LT-{####} it simply works, with no new settings and no migration.

validateFormat() allows exactly one number token, {YYYY} {YY} {MM} {DD} {COMPANY}, and letters, digits and - _ . /. {TYPE} is refused β€” it's a ticket type. The 64-character column limit is checked against a rendered six-digit number with a long stand-in company code.

3. πŸ”’ The counter

INSERT INTO asset_tag_counters (counter_key, next_value) VALUES (?, ?)
    ON DUPLICATE KEY UPDATE next_value = LAST_INSERT_ID(next_value + 1)

The same statement, and the same rowCount() trap, as TicketNumbering::claimNext() β€” read that method's comment. next_value holds the last number issued. Keys: asset:co<id> per company (0 = Default, so never NULL), or asset for global.

  • Uniqueness is proven, not assumed. After claiming, generate() checks the tag against the company's assets; if it is taken (a restored backup, tags typed by hand) it winds the counter forward with a doubling stride β€” but the first retry lands on the very next number ($seq + $step - 1), because stickers are printed in runs and a needless gap reads as a missing asset. Ticket numbering skips one there; tags don't.
  • Forward only. setNextNumber() and every wind use GREATEST(). A number on a sticker is never handed out again.
  • Rolls back with a failed asset. createWithTag() claims the number inside the transaction that inserts the asset, so a failed insert gives it back. If the caller already has a transaction, it joins it and neither commits nor rolls back itself. (Sandy's requirement, kept.)

4. πŸ”’ One lock for every write of a tag

Per-company uniqueness lives in code (assetTagAvailable()), because a UNIQUE (tenant_id, asset_tag) index cannot hold for the Default company β€” see the NULL trap. A check-then-write must be serialised, and every path that writes a tag goes through AssetTagsService::withLock() β€” a MySQL named lock, freeitsm_asset_tag_co<id>, which fails closed:

Path Method
A tag typed on create (screen, REST, import) createAsset() β†’ createWithTag()
A generated tag (any creator, agents and Intune included) createWithTag() β†’ generate()
A tag changed on an existing asset assign() (save_asset_tag.php)

Two paths with two locks β€” as the branch first had it, a lock for typed tags only β€” would let a typed AST-00005 and a generated AST-00005 both through.

noteManualTag(): a typed tag with the generated shape and a number ahead of the counter winds the counter past it, so the generator never has to skip it later.

A clash names the asset that has the tag (clashMessage()): "That tag is already on LAPTOP-12."

5. πŸ§ͺ Testing it

AssetTagsService::withSettings([...]) / forget() is the house override (TicketNumbering's pattern), so a test never writes the install's real settings. php tests/asset-tag-numbering.php β€” 29 checks: formats and preview; a failed create returns its number (the one check run with the service owning its transaction, failing before anything is written); consecutive tags; a taken tag skipped without a gap; typed tags kept, winding the counter, refused on a clash with the other asset named; assign() with history; forward-only; scope keys; off means no tag; and an agent-found asset tagged too. Everything else is in one rolled-back transaction, rows prefixed ZZTAG-.

6. Not built (and why)

  • Bulk renumbering β€” ticket numbering has it as a migration tool; asset tags are on physical stickers, so renumbering means re-labelling. Wait until somebody needs it.
  • Immutable tags β€” the QR token is the asset's permanent physical identity, so a tag can change (with history) and every printed label still scans.
  • Per-type or per-location rules β€” the format string leaves room for tokens; none are needed yet.

See also: Asset reconciliation β€” Developer Guide Β· QR asset labels β€” Developer Guide Β· The review (PR #164) Β· Developer Tests β€” Assets

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally