Matching algorithm
Every GSTR-2B document and every books document becomes a Doc (see models.py). Matching happens only between documents with the same GSTIN and the same kind (invoice, credit/debit note, import). Credit notes never match invoices.
1. Normalisation
Invoice numbers are reduced to a comparison key (normalize_invoice):
- upper-case;
- remove fiscal-year tokens:
2024-25,24-25,2024/2025(only when the second number really is the next year, so90/26survives); - remove every non-alphanumeric character;
- strip leading zeros of each digit run;
- drop a generic prefix (
INVOICE,INV,BILL,TI, ...).
| Raw | Key |
|---|---|
INV/2024-25/0012 |
12 |
INV12 |
12 |
inv-0012 |
12 |
S90/26-27 |
S90 |
A/25-26/7 |
A7 |
VX260900123 |
VX260900123 |
Amounts are rounded half-up to paise; dates accept dd-mm-yyyy, ISO, dd-Mon-yy, Excel serials and profile-specific formats. GSTINs are validated with the mod-36 algorithm: each of the first 14 characters is mapped to 0-35, multiplied by 1 (odd position) or 2 (even), the product p becomes p // 36 + p % 36, the sum s gives check character (36 - s % 36) % 36.
2. Passes
Each pass considers only still-unmatched documents. Within a pass, all acceptable (2B, books) pairs are scored and assigned greedily from best to worst, so the closest partner wins regardless of file order.
| # | Name | Accepts a pair when | Score (lower wins) |
|---|---|---|---|
| 1 | exact |
same key, same date, taxable and each tax head within Rs 1 | all equal |
| 2 | gstin_invoice |
same key (amounts, date, head may differ) | amount difference + date gap |
| 3 | gstin_amount_date |
taxable and total tax within Rs 1, date within 3 days | date gap + tax difference |
| 4 | fuzzy_invoice |
Levenshtein similarity >= 0.85, date within 45 days, taxable within 5%, and neither key is a prefix of the other | 1 - similarity |
| 5 | one_to_many |
see below | first subset found |
Pass 4 has two guards that came from real failure modes. Sequential invoices from one vendor (...212 and ...218) are 90% similar, so a similar taxable value is required. A prefix relation (INV9 and INV9/1) is the signature of a split invoice and is left to pass 5.
One-to-many (pass 5)
For each unmatched 2B document, search the unmatched books documents of the same GSTIN for a subset whose sums match; then the reverse (one books document against several 2B documents). Bounds keep it cheap and predictable:
- candidates: taxable not above the target (plus Rs 1) and date within 45 days, nearest dates first, at most 12;
- subset size 2 to 4 (at most C(12,4) = 495 combinations);
- taxable sum and total tax sum must each be within Rs 1 per document in the subset;
- all documents must use the same tax head.
Worked example: 2B has INV9 for 10,000 + 1,800 tax. Books have INV9/1 (4,000 + 720) and INV9/2 (6,000 + 1,080). Pass 1 to 4 find nothing (prefix guard), pass 5 finds {INV9/1, INV9/2}: taxable 10,000 and tax 1,800, so one MATCHED record holds one 2B id and two books ids.
Documents left over become ONLY_IN_2B or ONLY_IN_BOOKS.
3. Status classification
A pair found by any of passes 1 to 4 is classified by compare, checking in this order and keeping all reasons found:
TAX_HEAD_MISMATCH: one side charges IGST only and the other CGST+SGST only (place-of-supply problem);RATE_MISMATCH: taxable values agree but the effective rate differs (declaredrtfor 2B, otherwise tax/taxable snapped to the nearest GST slab);AMOUNT_MISMATCH: any of taxable, IGST, CGST, SGST, cess differs by more than Rs 1;INV_NO_MISMATCH: keys differ (found by pass 3 or 4);DATE_MISMATCH: dates differ;- otherwise
MATCHED.
Example: 2B TX-386 charges CGST 11,190.10 + SGST 11,190.10 on 447,603.98; books record IGST 22,380.20 on the same value. The pair is found by pass 2 and classified TAX_HEAD_MISMATCH.
4. ITC amounts per match
See gst-rules.md. For a paired match: claimable = min(books ITC, 2B ITC) (credit cannot exceed what 2B shows), zeroed for blocked, ineligible, time-barred or 2B-unavailable items, minus any 180-day reversal. at_risk is the booked ITC not supported by 2B. ONLY_IN_2B reports itc_unbooked. Credit notes carry a negative sign.
5. Carry-forward
After a run, every ONLY_IN_BOOKS document that is ITC-eligible, not reverse charge and not a credit note is stored in the SQLite carry_forward table with its origin period. The next run loads all pending items from earlier periods, adds them to the books side, and marks the ones that matched as resolved. Re-running a period is idempotent: items already resolved by that period are loaded again, and the period's own entries are rebuilt.
6. Explain
explain replays nothing: it reads the stored run. For paired documents it prints the side-by-side values, the reasons and the pass that matched. For unmatched documents it ranks same-GSTIN candidates on the other side (similarity, amount closeness, date gap) and shows what status a pairing would have had.