Issued invoices must be kept, and kept unchanged. The customer app from the series Building a customer app in Uniface 10 can already render an invoice as HTML. The last stage added an archive: all issued invoices and credit notes of a year are written to files, each file gets a SHA-256 checksum in the database, and a second operation checks later whether every file still exists and still has the same checksum.
The code is short. The interesting part is what the checksum can and can't tell you.
Tested with Rocket Uniface 10.4.03 Community Edition and SQLite 3.50.2, October 2026.
The table
CREATE TABLE ARCHIVE_ENTRY (
ARCHIVE_ID INTEGER NOT NULL PRIMARY KEY,
DOC_ID INTEGER NOT NULL,
ARCHIVE_YEAR INTEGER NOT NULL,
FILE_NAME VARCHAR(260) NOT NULL,
CHECKSUM VARCHAR(64) NOT NULL,
ARCHIVED_AT DATETIME NOT NULL,
ARCHIVED_BY VARCHAR(30)
);
CREATE UNIQUE INDEX UX_ARCHIVE_DOC ON ARCHIVE_ENTRY (DOC_ID);
CREATE INDEX IX_ARCHIVE_YEAR ON ARCHIVE_ENTRY (ARCHIVE_YEAR);
One row per document, enforced by the unique index. A SHA-256 hash in hex has 64 characters.
Archiving a year
ARCHIVE_DOCS(pYear, pDir) selects every issued invoice and credit note of the year - drafts never get archived - and writes one file per document, named after the document number:
sql/data "SELECT DOC_ID, COALESCE(DOC_NO, '') FROM SALES_DOC WHERE DOC_TYPE IN ('INVOICE', 'CREDIT') AND STATUS <> 'DRAFT' AND substr(DOC_DATE, 1, 4) = '%%(pYear)' ORDER BY DOC_NO, DOC_ID", "CUSTOMERS"
...
forlist vRow in vData
getitem vDocId, vRow, 1
getitem vNo, vRow, 2
if (vNo = "")
vNo = "DOC-%%(vDocId)"
endif
vNo = $replace(vNo, 1, "/", "-", -1)
vFile = "%%(vDir)%%(vNo).html"
activate "INVOICE_SVC".INVOICE_HTML(vDocId, vFile, vLines, pError)
if ($status < 0)
return -1
endif
call FILE_SUM(vFile, vSum)
...
sql "INSERT INTO ARCHIVE_ENTRY (ARCHIVE_ID, DOC_ID, ARCHIVE_YEAR, FILE_NAME, CHECKSUM, ARCHIVED_AT, ARCHIVED_BY) VALUES (%%(vId), %%(vDocId), %%(pYear), %%(vFileLit), '%%(vSum)', datetime('now', 'localtime'), %%(vUserLit))", "CUSTOMERS"
Two small defensive steps: a slash in a document number would become a directory separator, so it is replaced; and a document without number (which shouldn't exist once issued) still gets a unique file name.
The rendering is not new code. The archive calls the same INVOICE_HTML operation the user calls when printing an invoice, so the archived file looks exactly like the printed one.
Cancelled invoices are archived too. A cancelled invoice was issued and sent; it stays part of the record, next to the credit note that reverses it.
The checksum
entry FILE_SUM
params
string pFile : IN
string pSum : OUT
endparams
variables
string vText
endvariables
pSum = ""
vText = ""
fileload pFile, vText, "UTF-8"
if ($status < 0)
return -1
endif
pSum = $encode("HEX", $encode("SHA256", vText))
return 0
end
$encode("SHA256", …) returns the raw hash; $encode("HEX", …) turns it into the familiar 64-character string.
Checking the archive
ARCHIVE_INDEX(pYear, pFileName) goes through all entries of a year, recomputes each checksum and writes an index file with one line per document:
vState = "OK"
if ($lfileexists(vFile) != 1)
vState = "MISSING"
else
call FILE_SUM(vFile, vNow)
if (vNow != vSum)
vState = "CHANGED"
endif
endif
The result looks like this:
DocNo;File;Checksum;State
I-2025-00001;C:/Projects/CustomerApp/archive/2025/I-2025-00001.html;3f0c…a91e;OK
I-2025-00002;C:/Projects/CustomerApp/archive/2025/I-2025-00002.html;77b2…04cd;CHANGED
(checksums shortened here), and the operation returns the number of bad entries. The test archives a year with one issued invoice and one draft, expects exactly one entry with a 64-character checksum, verifies (OK), overwrites the file with other text and verifies again (CHANGED). The MISSING case is not covered by a test yet.
Limit 1: this hashes text, not bytes
fileload … "UTF-8" decodes the file into a string before hashing. The hash is therefore a hash of the text, not of the bytes on disk. Two consequences:
- The value is not what
sha256sumorGet-FileHashprint for the same file. Someone checking the archive with standard tools would see different numbers. (A byte order mark at the start of the file is one obvious difference.) - A change that keeps the decoded text identical - re-saving the file with a different encoding that represents the same characters - would not be detected.
For catching edits to the content, that's enough. For checksums that outside tools can reproduce, hash the bytes: load the file with fileload/raw into a variable of type raw and hash that. I described this in an earlier article (Six ProcScript surprises from 60 Uniface services). The archive should use it too; that's the first change on my list.
Limit 2: the checksums live next to the files
The files are in a folder; the checksums are in the database of the same application. Anyone who can change an archived file and also write to the database can update the checksum as well, and the check reports OK.
What the checksum does protect against is accidental change and change by someone who only has access to the file share. For more, the checksums must leave the system: print the index file, send it to someone, store it where the archive's writers can't write - or sign it.
Limit 3: archiving again replaces the old entry
ARCHIVE_DOCS removes an existing entry for a document before inserting the new one:
sql "DELETE FROM ARCHIVE_ENTRY WHERE DOC_ID = %%(vDocId)", "CUSTOMERS"
That makes the operation repeatable - archive a year, notice the folder was wrong, archive again. But it also means: if someone changes a file and then runs the archive for that year again, the new file is written from the database and gets a fresh checksum. The tampered file is overwritten, not detected.
For an archive that means "unchanged since the first time", the second run should refuse to touch documents that are already archived, or at least compare first and report differences. Both are small changes; neither is in this version.
So is it useful?
Yes, for what it is: a complete, readable copy of every issued document of a year, produced by the same code that prints invoices, with a cheap check that detects missing and edited files. It is not an audit-proof archive system, and the article title says "weaker than it looks" because the word checksum tends to make it sound like one.
Takeaways
Archive with the code that prints, so the archived file is the document the customer got.
Store one checksum per file and check for missing and changed files.
Hash bytes if anyone else should verify the checksums. fileload with an encoding hashes text.
Get the checksums out of the system if the people who can edit files can also edit the database.
Make re-archiving explicit. Repeating the run should not silently replace the evidence.







