# einvoice

GST e-invoice (IRN) JSON builder, validator and signed-QR toolkit for Indian businesses that must report B2B invoices to the Invoice Registration Portal (IRP). It converts ERP CSV/Excel exports into schema 1.1 e-invoice JSON through TOML mapping profiles, checks the result against the schema and the portal's business rules before upload, computes IRN hashes for duplicate pre-checks, and decodes and renders the signed QR code.

> Unofficial tool. It is not affiliated with or endorsed by GSTN, NIC or the IRP. The schema is a documented subset of version 1.1 and the rule codes are modelled on the portal's, not copied from it. Always validate on the official sandbox before relying on it.

## Features

- Mapping from flat CSV/XLSX (one row per invoice line) to `TranDtls`, `DocDtls`, `SellerDtls`, `BuyerDtls`, `DispDtls`, `ShipDtls`, `ItemList`, `ValDtls`, `PayDtls`, `RefDtls`, `ExpDtls`, driven by a TOML profile (column mapping, constants, value maps, date formats).
- Optional computation of missing amounts: gross, assessable value, CGST/SGST vs IGST by place of supply, Cess, item totals, invoice totals and round-off, using half-up rounding.
- Validation against a versioned JSON Schema (`einvoice/data/einvoice-1.1.schema.json`) and 37 business rules (38 coded checks including schema errors): GSTIN format and checksum, state codes, IGST vs CGST+SGST, HSN length by turnover, UQC units, item and invoice arithmetic with tolerances, date not in the future and inside the reporting window, unique document number per financial year, mandatory fields per supply type (B2B, SEZWP, SEZWOP, EXPWP, EXPWOP, DEXP).
- IRN computation (SHA-256 of seller GSTIN + FY + document type + document number) and duplicate detection.
- Signed QR JWT decoding (no signature check by default, RS256 verification with a public key), and QR rendering to PNG, SVG or the terminal.
- Batch mode with a per-issue error CSV, JSON output, and a status/supply type/rule summary.

## Install

Python 3.12+.

```bash
cd einvoice
uv venv .venv
uv pip install -p .venv/bin/python -e '.[dev]'
source .venv/bin/activate        # or call .venv/bin/einvoice directly
```

## Quickstart

```bash
einvoice build examples/invoices.csv -p examples/profile.toml -o out/invoices
einvoice validate out/invoices examples/resubmitted.json -c examples/einvoice.toml --errors-csv out/errors.csv
einvoice report   out/invoices examples/resubmitted.json -c examples/einvoice.toml
einvoice irn --gstin 29AAGCB7383J1Z4 --fy 2025-26 --type INV --no INV/001
einvoice qr decode examples/irp_response.json --public-key examples/qr_public_key.pem
einvoice qr render examples/irp_response.json -o out/qr.png
```

The examples contain 50 synthetic invoices (B2B, SEZ, exports, deemed exports, credit notes) with planted errors. They are dated October-November 2025, so `examples/einvoice.toml` pins "today" to 2025-11-15; without it the reporting-window rule would flag everything.

## Usage

### `build`

```
einvoice build SOURCE.csv|xlsx -p PROFILE.toml -o OUT
```

`OUT` ending in `.json` produces one array file, otherwise a directory with one file per invoice (`<seller gstin>_<doc no>.json`). Rows are grouped into invoices by the profile's `invoice_key`. Exit code 1 if any invoice could not be mapped (the others are still written).

```
$ einvoice build examples/invoices.csv -p examples/profile.toml -o out/invoices
built 50 invoice(s), 0 failed -> out/invoices
```

Excerpt of a generated file (`27XDAJH4657E1ZG_INV_2511_0013.json`; the profile left the tax amounts empty, so they were computed):

```json
"ItemList": [
  {
    "SlNo": "1",
    "PrdDesc": "Aerated water 200ml crate",
    "IsServc": "N",
    "HsnCd": "22021010",
    "Qty": 5.0,
    "Unit": "XYZ",
    "UnitPrice": 600.0,
    "TotAmt": 3000.0,
    "Discount": 150.0,
    "AssAmt": 2850.0,
    "GstRt": 28.0,
    "IgstAmt": 798.0,
    "CesRt": 12.0,
    "CesAmt": 342.0,
    "TotItemVal": 3990.0
  }
],
```

### `validate`

```
einvoice validate PATH... [-c CONFIG] [--turnover-crore N] [--window-days N] [--item-tolerance N]
                  [--invoice-tolerance N] [--today YYYY-MM-DD] [--errors-csv FILE]
                  [--format text|json] [-q]
```

`PATH` is a JSON file (one invoice or an array) or a directory of them. Exit code 1 if any invoice has errors; warnings alone exit 0. Duplicate document numbers are detected across everything in one run.

```
$ einvoice validate out/invoices examples/resubmitted.json -c examples/einvoice.toml --errors-csv out/errors.csv
(output trimmed to six of the 23 reported invoices)
07ACGHT1434U1ZC_INV_2510_0009.json  [INV/2510/0009]  INVALID
  E 2103  BuyerDtls.Gstin: GSTIN '27ABCDE1234F1Z' does not match the 15-character GSTIN pattern
07ACGHT1434U1ZC_INV_2511_0012.json  [INV/2511/0012]  WARNINGS
  W 2186  ExpDtls: ExpDtls is only meaningful for export supplies, not B2B
07ACGHT1434U1ZC_INV_2510_0015.json  [INV/2510/0015]  INVALID
  E 2161  DocDtls.Dt: Document date 20/11/2025 is after 15/11/2025
07ACGHT1434U1ZC_INV_2511_0039.json  [INV/2511/0039]  INVALID
  E 2123  ItemList[0]: SEZWOP supplies must carry zero CGST/SGST/IGST
27XDAJH4657E1ZG_INV_2511_0007.json  [INV/2511/0007]  INVALID
  E 2104  BuyerDtls.Gstin: GSTIN 36GCBYH5741C1ZA fails the checksum (expected check character E)
29DYUCV7912B1ZG_INV_2511_0017.json  [INV/2511/0017]  INVALID
  E 2162  DocDtls.Dt: Document is 75 days old; reporting window is 30 days
resubmitted.json  [INV/2511/0028]  INVALID
  E 2171  DocDtls.No: Document INV/2511/0028 already used in FY 2025-26 (first seen in 27XDAJH4657E1ZG_INV_2511_0028.json)
51 invoice(s): 28 valid, 2 with warnings, 21 invalid
```

The error CSV has one row per issue: `source,doc_no,status,severity,code,path,message`.

### `report`

```
$ einvoice report out/invoices examples/resubmitted.json -c examples/einvoice.toml
Invoices: 51   valid 28   warnings 2   invalid 21

By supply type
  B2B    invalid 16  valid 19  warnings 2
  DEXP   valid 2
  EXPWOP valid 2
  EXPWP  invalid 2  valid 2
  SEZWOP invalid 1  valid 1
  SEZWP  invalid 2  valid 2

By rule
  2001  x1   JSON schema violation
  2103  x1   Buyer GSTIN has invalid format
  2104  x1   Buyer GSTIN checksum is wrong
  ...
```

`--format json` prints the same data as JSON. `einvoice rules` lists the catalog.

### `irn`

```
$ einvoice irn --gstin 29AAGCB7383J1Z4 --fy 2025-26 --type INV --no INV/001
72242e6acf3b05a1b7f258e60c7a66dcbda2d4371755cd1677cfa9fba2c03b99

$ einvoice irn out/invoices/27XDAJH4657E1ZG_INV_2511_0028.json examples/resubmitted.json
27XDAJH4657E1ZG_INV_2511_0028.json  de1734373b099bb3c530f950329e99d90f8ddb698a9a26a89fbcade279e98374
resubmitted.json  de1734373b099bb3c530f950329e99d90f8ddb698a9a26a89fbcade279e98374  DUPLICATE of 27XDAJH4657E1ZG_INV_2511_0028.json
```

With files, the financial year comes from the document date. `--known FILE` (one IRN per line) flags `ALREADY REGISTERED`. Exit code 1 on any duplicate.

### `qr decode` and `qr render`

`SOURCE` can be a bare JWT, a file containing one, `-` for stdin, or an IRP response JSON with a `SignedQRCode` field (the response's `Irn` is cross-checked against the QR's).

```
$ einvoice qr decode examples/irp_response.json --public-key examples/qr_public_key.pem
Header: {"alg": "RS256", "typ": "JWT", "kid": "sample-key"}
  SellerGstin   27XDAJH4657E1ZG
  BuyerGstin    24MWJBR9785D1ZF
  DocNo         INV/2511/0001
  DocTyp        INV
  DocDt         03/11/2025
  TotInvVal     123456.0
  ItemCnt       2
  MainHsnCode   84136010
  Irn           6666b1189bfd2bf265cb31dcf888683c6d8b16a47efd1a006a4d799cb0fca26c
  IrnDt         2025-11-03 11:42:10
Signature: VALID
```

Without `--public-key` the signature line reads `not checked (no --public-key)`. A failed verification prints `INVALID` and exits 1. `--json` prints header, payload and decoded data.

```
$ einvoice qr render examples/irp_response.json -o out/qr.png     # .png or .svg
wrote out/qr.png
$ einvoice qr render examples/signed_qr.jwt --terminal
```

## Configuration

Validation settings come from a TOML `[validate]` table (`-c`), overridable per flag:

| Key | Default | Meaning |
| --- | --- | --- |
| `turnover_crore` | 5.0 | Aggregate turnover; above 5 crore HSN needs 6+ digits, otherwise 4+ |
| `window_days` | 30 | Max age of a document at reporting time; 0 disables the rule |
| `item_tolerance` | 1.0 | Rupee tolerance for item level arithmetic |
| `invoice_tolerance` | 1.0 | Rupee tolerance for invoice level sums |
| `today` | system date | Reference date for the future/window rules |

Mapping profiles are documented in [docs/mapping.md](docs/mapping.md); the rule catalog is in [docs/rules.md](docs/rules.md); algorithms and design decisions are in [docs/design.md](docs/design.md).

## Architecture

```
src/einvoice/
  cli.py        click commands (build, validate, report, irn, qr, rules)
  mapping.py    TOML profile, CSV/XLSX reading, grouping, coercion, amount computation
  schema.py     schema loading and error formatting
  data/         einvoice-1.1.schema.json
  rules.py      business rules, issue catalog, batch duplicate detection
  gstin.py      GSTIN format and checksum
  irn.py        IRN hash
  qr.py         JWT decode/verify/sign, QR rendering
  batch.py      loading invoice files, error CSV, summaries
  config.py     [validate] settings
  dates.py      portal date handling and financial year
  constants.py  state codes, UQC, supply types, GST slabs
examples/       sample data, profile, config, signed QR, generator script
tests/          pytest suite
```

## Tests

```bash
.venv/bin/python -m pytest -q
```

134 tests cover each business rule (positive and negative), schema errors, rounding tolerance boundaries, mapping profiles (types, dates, grouping, computation, errors), CSV/XLSX equivalence, IRN vectors, JWT decode/verify/tamper, QR rendering and the CLI end to end. `examples/make_examples.py` regenerates the sample data (`--qr` also makes a new key pair and signed QR).

## Limitations

- Implements a subset of schema 1.1: no `EwbDtls`, `AddlDocDtls`, `BchDtls`, `AttribDtls`; no IRP API client, authentication or upload.
- Rules approximate the portal's. Pincode-to-state matching, the master HSN list, per-HSN GST rate tables and GSTIN status lookups need data the tool does not ship.
- The 30-day window is applied to every document (the portal restricts it to larger taxpayers); set `window_days = 0` to turn it off.
- HSN must be 8 digits for exports; digit-length rules only, not HSN existence.
- Signature verification supports RS256 with a PEM public key only. The IRP's real public key must be obtained from the portal.
- Amounts are floats in the JSON output, rounded to two decimals; comparisons use the configured rupee tolerances.
