---
title: "An ERP where an invoice can't disagree with itself"
description: "Sales, dispatch, GST invoicing, purchasing with quality checks, inventory, and a double entry ledger with a Tally export."
url: /work/lab-distributor-erp
markdown: /work/lab-distributor-erp.md
case_study: true
updated: 2026-10-13
---

# An ERP where an invoice can't disagree with itself

The Scientific & General Agencies · Horizon Beam · Aug 2026 to Sep 2026

> I rebuilt the ERP a Jaipur lab chemicals distributor runs on, from the first lead to the ledger, in seven weeks.

| Fact sheet | |
| --- | --- |
| Client | The Scientific & General Agencies, India |
| Role | Wrote it on my own |
| When | Aug 2026 to Sep 2026 |
| Stack | Next.js, TypeScript, MongoDB Atlas with transactions, Auth.js, React PDF, Zod, Vitest, Playwright, Vercel |

## Headline numbers

- **seven weeks** from the first commit to the last

## The problem

The Scientific & General Agencies sells lab chemicals and equipment by catalogue number. Everything they do runs on one chain. A lead becomes a quotation, then an order, then one or more dispatches, an invoice for each dispatch, and a payment. On the other side, a shortage becomes a purchase requisition, a vendor PO, a goods receipt with a quality check, and stock.

They already ran a first version of this system, and we signed a scope to rebuild it: CRM and leads, quotations and orders, inventory and procurement, dispatch and invoicing, accounting and payments, attendance and tasks, ticketing and reports.

I started by writing down exactly what the system they already used did, screen by screen, so nothing they relied on would go missing.

## Constraints

- The money has to reconcile. An invoice total has to equal its lines, and the same number has to reach the GST report and the Tally export their accountant imports.
- Tax: CGST and SGST, or IGST, depending on where the goods go.
- An order ships in parts, and each dispatch gets its own invoice.
- GST doesn't allow editing an issued tax invoice, so corrections go through a credit note and only drafts can be edited.
- The client wanted to stay on MongoDB.
- Some questions to the client were still open when building started, so I couldn't wait on them.

## Decisions

1. **Keep MongoDB, but make it behave like an accounting database**, over the PostgreSQL my first spec asked for, because the client chose MongoDB Atlas, so the ledger, stock allocation and document numbering run inside transactions on a replica set, totals are recomputed before every save, and a save is rejected if they don't match the lines.
2. **Money in whole paise, with one function doing all the arithmetic on it**, over storing totals as entered and trusting them, because the quotation, the order, the invoice, the credit note, the vendor PO and every export read the same totals, and grand total and amount due are derived, never typed in.
3. **Weighted average cost**, over first in, first out (FIFO), because they sell by catalogue number, so the same product at a new price is a price change, not a separate batch, Indian accounting standards allow it, and it's Tally's default, so the export doesn't create a difference the accountant has to explain every month.
4. **Vendor wise stock as a record of where each unit came from**, over separate shelves per vendor, because the ask had two readings weeks apart in build effort, and segregating stock rewrites valuation and reports, so it should be priced on its own.
5. **Flag an order line whose vendor doesn't supply the product, with one switch to make it a rejection later**, over rejecting it from day one, because the existing data was full of default vendor choices nobody had made deliberately, so a hard rule would have started refusing lines people type every day.
6. **Never rewrite a signed document**, over updating the order when a shortage is bought from another vendor, because the order the customer signed keeps its original catalogue number, and the sales owner gets a notice instead.
7. **Cheap defaults for the questions the client hadn't answered yet**, over stalling the build, because each default can be undone with a settings change and is flagged in the app.
8. **Hosting on Vercel**, over hosting on Netlify, because the app leans on Next middleware and Server Actions, the features that suffer on an adapter that trails Next releases.

## How it works

Sales and procurement live in one workspace. An order's status is worked out from its line quantities on every change, so two screens can't disagree about it. A dispatch invoice bills only that dispatch.

FIFO stays reachable, because costing is read from one function. The consequence I told them about: a purchase price is now required on every goods receipt, because a missing price would set the stock's cost to nothing.

Scheduled jobs send reminders, watch ticket deadlines, flag low stock and products that have been unavailable too long, age invoices, chase overdue payments and re check every financial document. They're written and tested, and switching on their schedule needs a paid hosting plan.

## One order, start to finish

Every box is a part of the ERP: selling, dispatch and GST invoicing, buying with a quality check on what arrives, and the ledger under all of it.

- Lead and deal → Quotation
- Quotation → Sales order
- Sales order → Dispatch (part or whole)
- Dispatch (part or whole) → GST invoice (one per dispatch)
- GST invoice (one per dispatch) → Payment (and advances)
- Payment (and advances) → Double entry ledger (CGST, SGST, IGST, Tally export)
- Purchase requisition → Vendor PO
- Vendor PO → Goods receipt (quality check)
- Goods receipt (quality check) → Stock (weighted average cost): passes QC
- Stock (weighted average cost) → Dispatch (part or whole)

## Results

- Shipped: leads, deals, quotations, orders with partial dispatch, invoices per dispatch, payments and advances, a double entry ledger with GST and a Tally export, purchase requisitions, vendor POs, goods receipts with quality rejection, inventory, vendors, clients, tenders, tickets, attendance, reports, audit logs and notifications.
- Every item the client raised went into an issue log and was checked against the running app.
- An end to end browser suite maps one to one onto the acceptance checklist, so a failing test names the requirement that broke.

## What's still open

Batch numbers and expiry dates aren't built. For a lab chemicals distributor that's the real argument for stock lots, and nothing built now gets in the way of adding them. The costing method still needs the client's final yes.

_Named with the client's approval. Its numbers, customers and screens aren't shown._

## Contact

- Email: hi@namanbuilt.com
- LinkedIn: https://www.linkedin.com/in/naman-jain-hub

Horizon Beam is taking on new projects.
