Tallyard documentation
A user manual for the buyer and the stores clerk, a walkthrough of one working day, and the technical reference for whoever deploys it.
Demo access
The hosted demo is gated. Open Request access, leave your email, and Tallyard sends you the access PIN together with a temporary login that works for two days. Enter the PIN at /demo (or the short link /app), sign in, and you are on the purchasing desk of one synthetic plant, Godavari Foods Pvt Ltd, with fourteen suppliers and forty purchase orders already on file.
The temporary login has the admin role, so every screen is open to you, including the Users page. The public site always stays at /; the signed-in desk lives at /desk.
Accounts & roles
Who uses it. Tallyard is an internal tool for one buying organisation. Each person signs in with their own account, and every change is recorded against their username in the activity log.
Getting in. On a fresh installation the first person to open the app creates the admin account. After that, sign-up is by invitation only: an admin opens Users, enters a colleague's email and role, and Tallyard emails a join link (or shows the link on screen when no email channel is configured). The colleague picks a username and password and is in. On the public demo, access requests issue temporary logins instead, so nobody has to be invited to look around.
Roles. Admin can do everything and manages users. Buyer owns vendors and purchase orders, including emailing them to the vendor. Stores records deliveries at the dock and can read everything else. Buttons a role cannot use are not shown, and the server refuses the action regardless.
Vendors do not sign in. They receive the purchase order by email and confirm it with one click on a signed link, which needs no account. Admins can change a user's role at any time and deactivate accounts that should no longer sign in; nobody can deactivate themselves.
Vendors
Directory. Search by name, GSTIN or city; filter by category (raw material, packaging, maintenance, logistics, services, utilities) and status (prospect, active, on hold, retired). Each row shows payment terms, open purchase orders, the rupee commitment still to be received, and the on-time rate.
Adding a vendor. Legal name and category are required. A GSTIN must be fifteen characters in the statutory shape (two-digit state code, the ten-character PAN, an entity number, the letter Z, a check character); a PAN must be ten characters; and if both are given, the PAN embedded in the GSTIN must match the PAN. Payment terms are whole days from 0 to 180.
Onboarding checklist. A vendor cannot be made active until the required documents are ticked: GST registration certificate, PAN card, bank proof and a signed supply agreement. Raw-material and packaging suppliers (anything that touches food) additionally need an FSSAI licence, and its expiry date drives a risk flag sixty days before it lapses.
Statuses. Prospect → active → on hold ↔ active → retired. A retired vendor cannot be reactivated; add a new record if the relationship restarts.
Purchase orders
Composer. Pick a vendor (only active and on-hold vendors are offered), set an expected delivery date, then add lines: item, HSN code (4, 6 or 8 digits), quantity, unit, unit price and GST rate (0, 5, 12, 18 or 28 percent). The page previews totals as you type; the server recomputes subtotal, GST and grand total from the lines when you save and ignores anything else.
Numbering. Orders are numbered PO-<year>-NNNN, sequential within the year, assigned on save.
Lifecycle. Status describes delivery, not payment. An order is Not sent yet until you send it to the vendor; it then reads Sent to vendor and, once they confirm, Vendor confirmed. As goods arrive it becomes Partly delivered or Delivered on its own, and a delivered order can be marked Completed. Cancelling is allowed only while no goods have moved. A legend on the register explains every status; payment terms (net N days from invoice) are settled by accounts outside Tallyard. Illegal moves are refused with a message, not silently ignored.
Emailing the vendor. Once an order is sent, "Email to vendor" mails the PO to the vendor's address on file, with the lines, totals, delivery date, payment terms, and a Confirm this order button. The button is a signed link, valid for thirty days, that needs no login. When the vendor clicks it the order moves to Vendor confirmed on its own and the activity log records "confirmed by vendor via email link". "Resend email to vendor" sends it again with a fresh link. If the vendor replies by phone instead, the buyer can still press "Vendor confirmed" by hand. On this public demo every email is delivered to one demo inbox rather than to the made-up vendor addresses.
Document. "Print document" renders the order as a PO the vendor can act on: buyer and vendor blocks with GSTIN and PAN, delivery address, terms, HSN lines with taxable value and GST, and the grand total in words in the Indian system (lakh, crore).
Receiving
On an issued order, the stores clerk posts a goods receipt: a date, an optional GRN number (one is generated if left blank), and a quantity per line. Partial quantities are normal; a quantity above what is still outstanding is refused. Each posting is a receipt row, and the order's status is derived from the receipts every time the page loads.
The Receiving queue groups open orders into overdue, due this week and later, sorted by expected date, so the week's dock schedule is one page.
Scorecards & flags
| Metric | Definition |
|---|---|
| On-time rate | Share of fully received orders whose last receipt date is on or before the expected date. |
| Fill rate | Quantity received ÷ quantity ordered, over orders that are complete or past their expected date. |
| Lead time | Mean days from issue to last receipt, over fully received orders. |
| Price stability | 1 − mean coefficient of variation of unit price, over items ordered two or more times (draft and cancelled orders excluded). |
| Acknowledgement lag | Mean days from issue to vendor acknowledgement. |
Every metric shows the evidence it rests on ("3 received orders"). A metric with no evidence shows a dash rather than a fake number.
Risk flags on the dashboard: spend concentration (one vendor above 40% of the last ninety days' spend), documents (FSSAI licence expired or within sixty days), and delivery (on-time rate under 70% over at least three received orders). Each flag links to its vendor.
Assistant
The Assistant button opens a drawer with three assistants. All three work offline through a rules engine; when a model key is configured the same request goes to the model first, and its answer is validated against the same schema the rules engine produces. The badge at the top of the drawer names the engine that actually answered.
- Add a vendor from an email. Paste a supplier's email or brochure. You get a vendor draft: legal and trade name, category, GSTIN, PAN, contact, terms, plus risk notes (no GSTIN found, credit longer than policy, food-contact supplier without FSSAI). "Open in the vendor form" prefills the form; you still review and save.
- Order by typing a sentence. "order 500 kg maida from Balaji Flour Mills, deliver in 10 days" resolves the vendor by fuzzy match, prices the line from the vendor's last purchase of that item, and opens the composer prefilled as a draft. An unknown vendor is reported, not guessed.
- Summary of this vendor. On a vendor's profile, one paragraph over the scorecard with one recommended action. When a model writes it, the paragraph is accepted only if it quotes every metric value shown on the page.
A day at the desk
- 08:30, the buyer opens the dashboard. Seven receipts are overdue and two issued orders have no acknowledgement. She opens the oldest overdue order and calls the vendor.
- 09:10, a new supplier's email arrives. She pastes it into Assistant → Add a vendor from an email. The draft shows a valid GSTIN, net 45 days and a note that no FSSAI licence was mentioned. She saves the vendor as a prospect and ticks the two documents that came attached.
- 11:00, flour is running low. "order 1000 kg maida from Balaji Flour Mills, deliver in 7 days" → the composer opens with the line priced at last purchase. She adds atta on a second line, saves the draft, reviews it, issues it and prints the PO for the vendor.
- 15:40, a truck arrives at the gate. The stores clerk opens the order from the receiving queue and posts 800 kg of the 1000 kg maida against GRN-2609-041, note "short delivery". The order flips to partially received; the vendor's fill rate moves.
- 17:00, month-end review. The buyer opens Deccan Spices' profile: on-time 67% over three orders, an FSSAI licence expiring in 32 days. The vendor brief recommends a recovery plan before the next order. She exports orders to CSV for finance.
Architecture
Tallyard is a single FastAPI process rendering Jinja2 templates over a single SQLite file, with plain CSS and a small amount of vanilla JavaScript for the composer's line editor and the assistant drawer. There is no build step and no front-end framework.
| Module | Responsibility |
|---|---|
app/db.py | Connection, idempotent schema (vendors, purchase_orders, po_lines, receipts, events), event logging. |
app/domain.py | Validators (GSTIN, PAN, terms, lines), the PO state machine, totals, rupees in words, scorecard maths, dashboard, spend, risk flags, receiving queue. |
app/auth.py | Users, PBKDF2 passwords, roles, signed sessions and signed one-click links. |
app/mail.py | Outbound email through Resend, with an on-screen fallback and the demo sink. |
app/copilot.py | Provider layer (Anthropic → OpenRouter → heuristic) and the three assistants with their offline rules. |
app/seed.py | Deterministic synthetic seed: an Indian packaged-foods plant's supplier base. |
app/main.py | Routes, forms, CSV exports, JSON API, assistant endpoints. |
api/index.py | Hosted-twin wrapper: betadoc site at the root, PIN gate, the app behind it. No application code changes. |
The three rules
Receipts are the truth. The status column stores only human decisions (draft, issued, acknowledged, closed, cancelled). Partially received and received are computed from receipt rows on every read, as are open commitment and every scorecard metric. Nothing about goods movement is cached.
The assistant works without AI. copilot.provider() picks an engine from the environment; every call is wrapped so a failing or misbehaving model falls back to the rules engine, and the response carries the engine that answered. Model output must validate against the heuristic's schema or it is discarded.
Everything exports. /export/orders.csv, /export/vendors.csv, the JSON API, and the printable PO.
Configuration
| Variable | Meaning |
|---|---|
TALLYARD_DB | Path to the SQLite file (default tallyard.db beside the app; /tmp/tallyard.db on the twin). |
TALLYARD_SEED | 1 seeds an empty database with the synthetic plant. |
FEED_TOKEN | Token for the JSON API; random per process if unset. |
ANTHROPIC_API_KEY | Enables the Anthropic provider (official SDK, structured JSON output; model via TALLYARD_MODEL, default claude-opus-5). |
OPENROUTER_API_KEY | Enables the OpenRouter provider (model via TALLYARD_OPENROUTER_MODEL). |
RESEND_API_KEY, RESEND_FROM | Enables outbound email through Resend (order emails, invites). Without them the email is shown on screen to copy. |
TALLYARD_BASE_URL | Public address used inside confirm and invite links. |
TALLYARD_MAIL_SINK | Demo only: deliver every email to this one inbox instead of the recipient. |
SESSION_SECRET | Signs session cookies and confirm links; set it explicitly on any multi-instance host. |
BETADOC_PIN, BETADOC_SHOW_PIN, OWNER_EMAIL | Twin only: the demo PIN (sent by email on request, never shown unless BETADOC_SHOW_PIN=1) and who gets notified of access requests. |
TALLYARD_SHOW_DEMO_LOGINS | Whether the login page lists the seeded demo accounts (on locally, off on the public twin). |
# local
uv venv .venv && uv pip install --python .venv/bin/python -r requirements.txt
.venv/bin/uvicorn app.main:app --port 8341 # or: pm2 start ecosystem.config.js
.venv/bin/pytest -q # 41 tests
.venv/bin/python tools/walk.py # headless walk, 0 console errors
API reference
Machine consumers do not hold the PIN cookie; the API is authenticated by a feed token passed as ?token= or Authorization: Bearer. Without it the API answers 401 with a JSON body.
| Endpoint | Returns |
|---|---|
POST /orders/{id}/send | Emails the PO to the vendor (buyer/admin session). Returns {mode, ok, confirm_url} for JSON callers. |
GET /confirm/{token} | Public. The vendor's one-click confirmation; moves a sent order to Vendor confirmed. |
GET /healthz | {ok, app, db, vendors, orders, ai, email, users, time} — ai names the assistant engine in use (anthropic, openrouter or heuristic). |
GET /api/v1/vendors?token=… | {count, vendors:[{id, code, legal_name, trade_name, category, gstin, status, net_days, city, state}]} |
GET /api/v1/orders?token=… | {count, orders:[{id, number, vendor, status, expected_date, total, overdue, lines:[{item, hsn, qty, unit, unit_price, received}]}]} — status is the derived delivery status (draft, issued, acknowledged, partially_received, received, closed, cancelled). |
POST /copilot/intake {text} | {draft:{…}, provider, notes} |
POST /copilot/po {text} | {vendor_id, vendor_name, match_score, lines:[…], expected_date, issues, provider} |
GET /copilot/brief/{vendor_id} | {text, action, provider, metrics} |
GET /export/orders.csv, /export/vendors.csv | CSV downloads (session-gated on the twin). |
Known limits
- Three fixed roles (admin, buyer, stores); there are no per-plant permissions or approval chains yet.
- The hosted twin's database is ephemeral by design; the on-premises lane is the stateful one.
- No invoice or payment tracking yet; the desk stops at goods receipt.
- Vendor and item matching in the assistant is fuzzy string matching; ambiguous names should be checked in the directory.
- Model providers are optional and unconfigured on the public demo, so the badge reads "Works without AI" there.