← All work

Case study · Francellen Enterprise

Retail point-of-sale & inventory management

A complete till and stock-control system, built as a single Flutter codebase that runs on the Windows machine at the counter and on Android phones away from it. It replaces the paper ledger end to end — and it is handling live takings and live stock today.

In production Windows + Android ~26,000 lines of Dart · 152 files
Client
Francellen Enterprise ↗
Role
Design and development
Stack
Flutter, Dart, Firebase, Cloud Firestore, Riverpod
Tests
42 suites — receipts, pricing, cart, printers

The problem

The shop ran on a paper ledger and a calculator. That worked until it didn't:

  • Stock counts were guesses. Nobody could say what was on the shelf without walking to it, so reordering happened after something had already run out.
  • Credit was a notebook. Goods went out on trust with the amount written down, and chasing what was owed meant reading back through pages by hand.
  • The day's numbers took an evening. Takings, cost and profit had to be totalled manually, which meant they were known a day late, if at all.
  • Receipts were handwritten, which is slow at a counter with a queue.

They needed one system that did all of it, ran on the hardware they already owned, and could be used by staff with no software training.


What was built

Selling

A till screen built for speed at the counter: type-ahead product search, a running cart, quantity and price adjustment per line, and cash-and-change calculation. Unfinished baskets can be parked as named drafts and picked up again, so a customer who goes back to their car doesn't block the queue.

Every completed sale prints a receipt and writes the stock movement in the same step, so the shelf figure can never drift from what was actually sold.

Stock and catalogue

Products carry cost price, selling price, brand, category and supplier. Stock records the quantity on hand against a minimum required quantity, which is what drives reorder warnings. Purchases from suppliers feed stock back in, and inventory value is reported at both cost and retail — so the owner sees margin on the shelf, not just units.

Products that were never categorised are surfaced deliberately rather than hidden, so category reporting always adds up to the full takings.

Credit, money out, reporting

Goods can go out on credit against a named customer with a due date and a running status, with outstanding balances tracked per customer — and credit sales print the same receipt as cash sales, so the customer leaves with a record. Expenses and supplier purchases are recorded alongside sales, which makes the profit figure a real one: takings minus cost of goods and minus what was spent running the shop.

The dashboard covers takings, profit, what's owed, best-selling products and best-performing categories — each ranked two ways, by packs moved and by money brought in. A cheap product can top the pack count and still be worth a fraction of an expensive one, so both views are kept. Three access levels — owner, admin and ordinary staff — with the financial reporting gated behind the owner's account.


Engineering notes

A few decisions worth drawing out, because they're where most of the work actually went.

Thermal receipt printing, done properly

Receipts print as raw ESC/POS — the printer's own command language — rather than as an image or a PDF handed to a driver. That meant building the receipt layout from scratch: the money columns are measured against the widest value on that particular receipt and the product name is given whatever width is left, so a long product name wraps onto its own indented lines instead of shoving the total off the edge of the paper.

The same document renders through three paths depending on what the device has: Bluetooth on Android, raw Windows spooler printing at the till, and a PDF fallback for sharing or reprinting. A calibration page prints rulers at each supported width, so a printer can be set up correctly without anyone measuring paper.

On the shop's 80mm printers the cutter sits about 15mm further along the paper path than the print head, so the last lines printed end up on the offcut. The receipt feeds past that distance before cutting — which is why the footer arrives intact.

Privacy at the counter

The home screen sits on the counter facing the room. Profit, outstanding credit and delivery costs were originally the first thing on it — visible to every customer and every member of staff standing there. Those figures moved to a separate page behind a deliberate tap, restricted to the owner. Today's takings stayed, because that's what the cashier actually needs.

This is the kind of requirement that doesn't appear in a specification and only turns up by watching the software be used in the room it lives in.

One codebase, two very different screens

The app was laid out for a till screen. On a desktop monitor that produced rows with the label at the far left, the buttons at the far right and a metre of dead space between them. A shared set of layout primitives caps content width, adds gutters and groups each section onto its own surface — so the desktop window reads properly without forking the UI or changing anything on mobile.

Windows deployment

Shipping Flutter to a Windows machine that isn't a developer's turned up its own problems. The till ran a Visual C++ runtime from 2021 while the build machine compiled against a much newer one; that runtime is backward compatible but not forward compatible, so the app crashed on first use of the database with a native access violation and no Dart-level error to read. The build now ships the matching runtime alongside the executable, which fixes it without anyone installing anything on the shop's machine.

Because native crashes like that never surface as a catchable exception, the project also includes a separate diagnostic build — a second entry point into the same app that walks through startup, database initialisation and querying one step at a time, writing a line per step. Whatever kills the real app kills it in the same place, and the last line written names the step. It runs automatically in CI on a clean Windows machine, which is what made it possible to prove the fault was specific to the shop's hardware rather than the code.

Build and delivery

A GitHub Actions pipeline builds the Windows release on every push, bundles the runtime, and publishes the installable build to a permanent download link. Each build stamps its commit into the app itself, so a report of "still broken" can be checked against which build is actually installed instead of guessed at.


Technical summary

FrameworkFlutter / Dart
State managementRiverpod with code generation
ModelsFreezed — immutable data classes with generated equality and copy
BackendFirebase — Cloud Firestore and Firebase Auth
Local storageShared preferences — signed-in session and printer configuration
PrintingESC/POS over Bluetooth and Windows raw printing, with PDF generation
Chartsfl_chart
Scale~26,000 lines of Dart across 152 source files
Tests42 test suites covering receipt layout, pricing, cart behaviour and printer handling

The data layer is split into one repository per domain — products, stock, sales, credit, customers, suppliers, purchases, expenses — rather than a single service class, which keeps each area's queries and caching in one readable place.


Outcome

The shop now takes every sale through the system. Stock levels are current rather than remembered, credit is tracked against named customers with due dates instead of a notebook, and the owner can see the day's takings, profit and best sellers from the phone without being in the shop.

The system holds live production data and has been running the business day to day.