Brian Gita / Work / Ptone

In production · Private client system

Ptone Shop Management

The system an agricultural produce business runs on every day: sales, stock, cash and profit, with the controls an auditor would ask for built in.

Role
Sole developer
In production
6 months
User roles
4
Automated tests
40

The problem

The business buys produce from suppliers by the tonne and sells it by the kilo, retail and wholesale. Customers pay in cash, Mobile Money, bank transfer or cheque. Some pay later, and suppliers get paid in parts.

Before this system, the owner could not answer three basic questions with confidence: how much stock should be in the store, how much money should be in each account, and which sales actually made a profit.

Prices change from one delivery to the next, so a single "cost price" per product gave the wrong profit figure for most sales.

How it works

Every action that moves goods or money leaves a record that points back to its source. When a sales attendant saves a sale, one database transaction does all of the following, or none of it:

  1. Locks the product row, so two sales can't sell the same kilos at once.
  2. Takes the quantity from the oldest stock deliveries first (FIFO) and records which delivery each kilo came from.
  3. Prices the sale from the list price, minus any per-kg discount, and stores the true cost of those specific kilos alongside it.
  4. Generates a numbered, printable invoice showing who processed it.
  5. Posts any payment to the chosen cash, bank or Mobile Money account as a ledger entry tagged with its source.

Receiving stock works the same way in reverse. A goods received note adds a new delivery at its own unit cost and tracks what's still owed to the supplier.

Architecture

A server-rendered Django monolith, split into six apps along the lines of the business:

salesInvoices, payments, receivables
stockProducts, deliveries, FIFO batches
bankingAccounts, ledger, transfers
expensesOperating costs, paid from accounts
reportsP&L, reconciliation, Excel export
usersRegistration, approval, roles

PostgreSQL in production and SQLite locally, deployed on Render behind Gunicorn, with WhiteNoise serving static files. A small business with a handful of staff doesn't need a SPA or a separate API, so pages stay fast and there's less to maintain.

FIFO costing

Each delivery is a batch with its own unit cost and a remaining_quantity. A sale draws from the oldest batches first and records the split, so the cost of every kilo sold can be traced back to a supplier invoice.

batches = StockReceipt.objects.select_for_update().filter(
    product=product, remaining_quantity__gt=0
).order_by("date_received", "id")

for receipt in batches:
    take = min(receipt.remaining_quantity, needed)
    receipt.remaining_quantity -= take
    total_cost += take * receipt.unit_cost
    needed -= take

Stock on hand is valued at what each remaining batch actually cost, not today's price. Stock that existed before FIFO tracking began is costed at the product's cost price at the time, and is reported separately rather than hidden.

Security and controls

I spent seven years testing other companies' controls, so I built these in from the start:

ControlHow it's enforced
Access approvalNew registrations are inactive until an admin approves them. Admins can deactivate a user or reset a password.
Brute-force protectionAfter five failed sign-ins, the account is locked for 15 minutes, even if the next password is correct.
Least privilegeFour roles: admin, store manager, sales attendant and accounts. Every view checks the role server-side, not just by hiding menu items.
Audit trailSales, ledger entries and transfers record who created them and when.
No silent deletesDeleting a sale or delivery posts reversing entries to the affected accounts, so balances always match their history. Accounts, products and deliveries that have transactions can't be deleted; products are archived instead.
IntegrityMoney and quantities use Decimal, never floats. Balance changes lock the account row inside a transaction.
Transport and secretsHTTPS redirect and secure cookies in production. The secret key and database URL come from the environment, not the code.

The reconciliation report

This is the report that answers the owner's three questions. For each product it compares kilos received minus kilos sold against what the system says is in stock, and shows any difference. For any date range it shows profit per kilo against the target margin the owner set, and lists every sale priced below it. The report exports to Excel for the accountant.

Fixing production safely

Real data finds real gaps. Two examples:

Deleted sales left money in the bank

Early on, deleting a sale returned the stock but didn't reverse the customer's payment, so account balances drifted. I fixed the delete path, then wrote a one-off command to post the missing reversals for sales already deleted. It refuses to run if the invoice still exists, skips any invoice already reversed so it's safe to re-run, and warns if a balance goes negative.

Switching live data to FIFO

Moving from a single cost price to FIFO meant recosting every past sale. A command replays every delivery and sale in date order and prints the old and new cost of goods sold per product before anything is saved.

Both commands run as a dry run by default and only save with --commit, inside one transaction. Before each change to production, I took a database backup.

$ python manage.py rebuild_fifo
Product                Old COGS      FIFO COGS       Change
...
Dry run, nothing saved. Re-run with --commit to apply.

Testing

40 automated tests cover the paths where a bug costs money: FIFO allocation across batches, sale edits and deletes that restore stock and reverse payments, supplier overpayments that are refused, Excel exports that must match the on-screen report, role access and login lockout. There's also a check that a malicious product name can't inject script into a page.