Vendor & purchase-order desk · for small manufacturers
Tallyard is where a plant's buyer raises purchase orders, the stores clerk posts goods receipts, and the vendor scorecard writes itself from what actually arrived. Built for an Indian packaged-foods plant: GSTIN on every vendor, HSN on every line, FSSAI on the checklist, rupees in words on the PO.
Leave your email once and we send one access PIN and one temporary login that open every OC Apps demo, this one included. The demo runs on synthetic data and reseeds itself; nothing you enter is kept.
| PO-2026-0039 | Deccan Spices | Acknowledged | 93,775 |
| PO-2026-0038 | Kirloskar Service | Issued | 14,19,440 |
| PO-2026-0035 | Deccan Spices | Received | 4,19,500 |
| PO-2026-0023 | Kaveri Oils | Partially received | 6,47,400 |
A plant with a dozen suppliers runs purchasing on a spreadsheet, a WhatsApp group and one person's memory. Three things go wrong every month.
Purchase orders live in a folder; receipts live in the stores register. Open commitment is a number someone recomputes by hand before the bank call.
"They're usually late" is not a fact you can renegotiate on. On-time rate, fill rate and lead time exist in the delivery challans, uncounted.
A supplier starts delivering before the GST certificate, bank proof or FSSAI licence is on file, and the audit finds out first.
Six screens and one drawer. Everything is server-rendered, fast, and exportable as CSV.
A searchable directory; GSTIN and PAN validated on entry; an onboarding checklist that gates activation; a profile with terms, open commitment, the five-metric scorecard and a timeline.
A line-by-line composer with HSN codes and GST rates; totals recomputed on the server; sequential numbering; a lifecycle that only moves forward; a printable PO with both GSTINs and the amount in words.
Goods receipts per line, partials allowed, over-receipts refused. The order's state is derived from its receipts, never typed in. A queue of what is overdue, due this week, and later.
On-time rate, fill rate, mean lead time, price stability and acknowledgement lag, computed at read time from receipts and pinned by a fixture test.
Open commitment, orders awaiting acknowledgement, overdue receipts, top vendors, twelve weeks of spend, risk flags that link to the vendor they name, and breakdowns by category, vendor and month.
Admin, buyer and stores sign in with their own accounts; every change carries a name. Orders are emailed to the vendor with a one-click confirm link, and the vendor's click sets "Vendor confirmed" without anyone at the desk typing it.
Paste a supplier's email to get a vendor draft. Type "order 500 kg maida from Balaji Flour Mills, deliver in 10 days" to get a priced draft PO. Ask for a one-paragraph vendor brief. Everything works without AI; when a model key is set it helps too, and the badge says which is in use.
One FastAPI process, one SQLite file, server-rendered pages with a little JavaScript for the composer and the assistant drawer. The same code serves the local service and the hosted twin.
Every capability we set out to build, with its status and the test that proved it. This table is generated from the internal build tracker, so it cannot claim more than the tracker shows.
41 of 41 tracked capabilities built and verified · 0 partial · 0 on the roadmap. Generated from the build tracker; each row names the test that proved it.
| Area | Capability | Status | Proof (acceptance test) |
|---|---|---|---|
| Foundations | Schema: vendors, purchase_orders, po_lines, receipts, events | Built | db.init() on an empty file creates exactly those 5 tables plus schema_version; running it twice is a no-op (row counts unchanged) |
| Foundations | Synthetic seed: an Indian packaged-foods plant's supplier base | Built | seeded DB holds >=12 vendors with valid-format GSTINs, >=30 POs covering every lifecycle status, >=60 lines with HSN codes, and receipts against issued POs; reseed on an empty DB is deterministic (same counts) |
| Foundations | App shell: FastAPI boot, /healthz, base layout with design tokens | Built | curl /healthz returns 200 JSON with ok=true, db path, vendor count and ai provider; GET / returns 200 HTML with the nav and no inline placeholder text |
| Foundations | Test harness | Built | pytest -q runs >=1 test against a temp DB via TestClient and passes |
| Vendors | Vendor directory with search, category and status filters | Built | GET /vendors?q=<seeded name fragment> lists only matching vendors; ?status=on_hold lists only on-hold; headless: table renders one row per seeded vendor with 0 console errors |
| Vendors | Vendor list filters combine | Built | GET /vendors?q=<frag>&category=<cat>&status=<st> returns only vendors matching all three; a filter that excludes everything renders the 'no vendors match' state |
| Vendors | Vendor create/edit with GSTIN + PAN + net-days validation | Built | POST /vendors with a 14-char GSTIN returns 422 with the field message; a valid form creates the vendor, writes a 'vendor.created' event and redirects to its profile |
| Vendors | Vendor profile: terms, open commitment, scorecard, checklist, timeline | Built | GET /vendors/{id} for a seeded vendor shows open-commitment ₹ equal to the SQL sum of its unclosed POs, the 5 scorecard metrics, the onboarding checklist and the last 8 events |
| Vendors | Onboarding checklist gates activation; status transitions guarded | Built | POST /vendors/{id}/status active on a vendor missing a required document returns 409; after ticking the documents the same request returns 303 and the status is active; retired->active is refused |
| Purchase orders | PO register with status chips, totals and read-time overdue flag | Built | GET /orders lists every seeded PO; ?status=issued filters; a PO whose expected date is past and not received carries the overdue class; totals column equals the stored grand total |
| Purchase orders | PO composer with multi-line editor; server-side totals and sequential numbering | Built | POST /orders with 3 lines creates PO-<year>-NNNN one higher than the last; subtotal, GST and grand total are recomputed server-side and match the line math to the paisa; a client-tampered total is ignored |
| Purchase orders | Lifecycle state machine: draft→issued→acknowledged→(partial)→received→closed; cancel guarded | Built | tests/test_lifecycle.py covers every legal edge and asserts 409 on issued->closed, received->cancelled and closed->anything |
| Purchase orders | Printable PO document (buyer/vendor blocks, GSTIN, HSN lines, terms) with print stylesheet | Built | GET /orders/{id}/document returns 200 containing both parties' GSTINs, every line's HSN, the words-in-rupees total, and a @media print rule that hides the nav |
| Purchase orders | JSON API v1 with feed token | Built | GET /api/v1/orders without token returns 401 JSON; with ?token=<FEED_TOKEN> returns 200 with count equal to the PO table; /api/v1/vendors likewise |
| Receiving | Goods receipt per line; partial allowed; over-receipt refused; PO status derives from receipts | Built | POST /orders/{id}/receive with qty > outstanding returns 422; receiving part of one line flips the PO to partially_received; receiving the remainder flips it to received; each posts a receipt row |
| Receiving | Receiving queue: due this week, overdue, by vendor | Built | GET /receiving lists issued/acknowledged/partial POs grouped overdue / this week / later with counts that equal the SQL groups for today's date |
| Receiving | Event timeline on every mutation | Built | creating a PO, issuing it and receiving a line write >=3 events with actor, kind, subject and timestamp; /activity renders them newest first |
| Scorecards & dashboard | Vendor scorecard metrics computed at read time from receipts | Built | tests/test_scorecard.py builds a 4-PO fixture and asserts on-time rate, fill rate, mean lead days, price stability and acknowledgement lag to 2 decimals against hand-computed values |
| Scorecards & dashboard | Dashboard: open commitment, awaiting acknowledgement, overdue receipts, top vendors, 12-week spend | Built | a test asserts each dashboard tile equals its SQL definition on the seeded DB; headless: 12 spend bars render with 0 console errors |
| Scorecards & dashboard | Spend analytics by category / vendor / month + CSV exports | Built | GET /export/orders.csv has PO-count + 1 rows and a GST column; /spend renders three breakdowns whose grand totals agree with each other |
| Scorecards & dashboard | Risk flags: spend concentration, expiring documents, poor on-time vendors | Built | the seeded DB raises >=2 flags of >=2 kinds; each flag on the dashboard links to the vendor it names |
| Copilot | Provider layer: Anthropic → OpenRouter → heuristic; never raises; badge in /healthz | Built | with no keys set /healthz ai=heuristic; a test monkeypatches a failing provider and the copilot still returns a heuristic answer with provider='heuristic' |
| Copilot | Vendor intake: paste an email/brochure → structured vendor draft prefilled into the form | Built | a sample supplier email yields legal name, GSTIN, PAN, category, contact and net-days via the heuristic extractor; LLM output is validated against the same schema and rejected on mismatch |
| Copilot | PO from a sentence: 'order 500 kg maida from <vendor>, deliver in 10 days' → draft PO | Built | the heuristic parser resolves vendor, item, qty, unit and due date from that sentence, prices the line from the vendor's last price, and creates a draft PO; unknown vendor returns a clear 'no such vendor' message |
| Copilot | Vendor brief: one paragraph on the scorecard with a recommended action | Built | the brief for a seeded vendor mentions each of the 5 metric values shown on the profile; offline it is a template, online it is LLM text checked for those values |
| Copilot | Copilot drawer UI with an honest provider badge | Built | headless: the drawer opens from any page, the badge text equals /healthz ai, and submitting the intake sample fills the vendor form fields with 0 console errors |
| Ship | Local service under pm2 on :8341 with an ecosystem file | Built | pm2 list shows tallyard online; curl http://127.0.0.1:8341/healthz returns 200 after pm2 restart tallyard |
| Ship | Hosted twin on Vercel: ephemeral /tmp DB that reseeds, stateless PIN gate | Built | curl https://<twin>/healthz returns 200 with ai provider; GET /app without the PIN cookie redirects to /demo; with the PIN the dashboard renders |
| Ship | Betadoc site served at / of the twin: story, product, architecture, honest feature matrix, roadmap | Built | GET / on the twin returns the site; its feature matrix marks built / partial / roadmap exactly as the task DB reads at publish time; documentation page links resolve |
| Ship | Custom subdomain tallyard.ajaykongala.online | Built | curl -sI https://tallyard.ajaykongala.online/ returns 200 over TLS and /healthz matches the vercel.app twin |
| Ship | Headless walk of every page: 0 console errors, forms submit | Built | tools/walk.py visits dashboard, vendors, a profile, orders, composer, a document, receiving, spend, activity and the copilot drawer on :8341 and reports 0 console errors and 0 failed requests |
| Ship | Registry, HOME.md, cockpit :6796 under pm2, memory updated | Built | tools/check_registry.py passes with 8341 and 6796 present; the cockpit's five tabs render with 0 console errors and the architecture boxes reflect task state |
| Ship | Plain-language delivery statuses with legend; Copilot renamed Assistant with simpler copy (user feedback 2026-09-18) | Built | PO_LABEL uses delivery words (Not sent yet / Sent to vendor / Vendor confirmed / Partly delivered / Delivered / Completed / Cancelled); /orders renders the legend stating payment is separate; order page action buttons use ACTION_LABEL; drawer titled Assistant with badge 'Works without AI' or 'Using AI'; pytest 41 green and tools/walk.py PASS; twin redeployed with the same wording |
| Users & email | Users table, PBKDF2 passwords, stateless signed sessions | Built | tests: create_user + authenticate succeed; wrong password and inactive user fail; a tampered session cookie reads as no session |
| Users & email | Sign-up / sign-in / sign-out pages; first sign-up becomes admin; later sign-ups need an invite code | Built | POST /signup on an empty users table creates an admin and signs in (303 + cookie); a second POST /signup without a valid invite returns 403; POST /login wrong password 401; /logout clears the cookie; every app page redirects to /login when signed out |
| Users & email | Roles gate writes: admin all, buyer vendors+orders+sending, stores receiving only; events carry the username | Built | a stores user POST /orders → 403 and POST /vendors → 403 but POST /orders/{id}/receive → 303; a buyer POST /users/invite → 403; the event written by a receipt has actor == that user's username |
| Users & email | Admin users page: list, invite by email (Resend or on-screen link), change role, deactivate | Built | admin GET /users lists users; POST /users/invite creates an invite and (screen mode) shows the join link; a deactivated user's POST /login returns 401; role change is reflected on the next sign-in |
| Users & email | Mail service: Resend when RESEND_API_KEY is set, on-screen preview otherwise; /healthz reports email mode | Built | with no key /healthz email=screen and mail.send returns mode=screen with a preview; a test monkeypatching urlopen sees the Resend request body carry from/to/subject; tallyard.ajaykongala.online verified as a Resend sending domain |
| Users & email | Email the PO to the vendor with a signed confirm link; the vendor's click sets 'Vendor confirmed'; resend button | Built | POST /orders/{id}/send on a sent order emails (or previews) the PO with /confirm/<token>; GET /confirm/<token> flips issued→acknowledged without a login and writes an event 'confirmed by vendor via email link'; a second click says already confirmed; a bad token 404s; the twin passes /confirm/ through the PIN gate |
| Users & email | Ship: seeded demo users, tests + walk green, twin redeployed with RESEND env, docs updated | Built | seed creates admin/buyer/stores demo users shown on the gate and in the docs; pytest and tools/walk.py pass with sign-in; twin /healthz shows email=resend; a real PO email lands via Resend from hello@tallyard.ajaykongala.online |
| Users & email | Email-first demo access: PIN hidden and rotated, request form issues PIN + temporary login by email, stateless guest credentials | Built | twin /demo shows no PIN and links to /demo/request; POST /demo/request with a valid email answers 'Check your inbox' and a Resend email with PIN + guest login lands; the guest login signs in on any instance (stateless HMAC credentials, 2-day expiry); login page on the twin lists no seeded credentials; docs no longer publish credentials |
What comes after the demo, in the order a plant would ask for it.
Two lanes, one codebase, no code changes between them. The twin is flipped with environment variables alone.
Runs on a plant PC or a small Linux box under a process manager. One SQLite file is the whole database; back it up by copying it. Model keys are optional and stay on that machine.
uv venv .venv && uv pip install -r requirements.txt pm2 start ecosystem.config.js # :8341 # optional: ANTHROPIC_API_KEY or OPENROUTER_API_KEY in .env
The same app on a serverless host behind a PIN gate, with an ephemeral database that reseeds itself on every cold start. Nothing to install, up around the clock, and safe to hand to a prospect because nothing persists.
TALLYARD_DB=/tmp/tallyard.db TALLYARD_SEED=1 BETADOC_PIN=… FEED_TOKEN=… vercel --prod # betadoc at /, app behind /demo
Synthetic data, real workflows. Raise a PO, receive it short, and watch the scorecard move.