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:
- Locks the product row, so two sales can't sell the same kilos at once.
- Takes the quantity from the oldest stock deliveries first (FIFO) and records which delivery each kilo came from.
- Prices the sale from the list price, minus any per-kg discount, and stores the true cost of those specific kilos alongside it.
- Generates a numbered, printable invoice showing who processed it.
- 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:
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:
| Control | How it's enforced |
|---|---|
| Access approval | New registrations are inactive until an admin approves them. Admins can deactivate a user or reset a password. |
| Brute-force protection | After five failed sign-ins, the account is locked for 15 minutes, even if the next password is correct. |
| Least privilege | Four roles: admin, store manager, sales attendant and accounts. Every view checks the role server-side, not just by hiding menu items. |
| Audit trail | Sales, ledger entries and transfers record who created them and when. |
| No silent deletes | Deleting 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. |
| Integrity | Money and quantities use Decimal, never floats. Balance changes lock the account row inside a transaction. |
| Transport and secrets | HTTPS 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.