GST invoices that survive your accountant's first review
The first feedback we got from a client's CA was that the invoice total was off by one rupee. The math was correct line by line. The problem was that we rounded the tax amounts independently and then summed them, while the CA's tally software rounded the final taxable value first and then computed tax on that. Both approaches are legal under Section 170 of the CGST Act, but you cannot mix them on the same invoice.
The rounding rule is straightforward in principle: round to the nearest rupee, with 50 paise going up. The question is what you round. GST law lets you round the taxable value per line, the tax per line, the total tax, or the grand total. What you cannot do is round intermediate values one way and final values another and present the result as a single invoice.
We standardised on this approach after that call: compute CGST and SGST per line item without rounding, sum them into a total tax amount per tax slab, round only the grand total, and show the unrounded tax breakdown in a separate column. The grand total is what the customer pays and what hits the GSTR-1 return. The per-line tax shown is for the accountant to verify.
A typical line item calculation looks like this:
from decimal import Decimal, ROUND_HALF_UP
def line_tax(item):
rate = Decimal(item['gst_rate']) / Decimal(100)
taxable = Decimal(item['quantity']) * Decimal(item['unit_price'])
cgst = (taxable * rate / 2).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
sgst = (taxable * rate / 2).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
return {
'taxable_value': taxable.quantize(Decimal('0.01')),
'cgst': cgst,
'sgst': sgst,
'line_total': (taxable + cgst + sgst).quantize(Decimal('0.01'))
}Use Decimal, not float. A 0.1% drift on a 12 crore monthly turnover is 12,000 rupees, and the reconciliation will fail.
The fields an accountant checks, in the order they check them, are: GSTIN of supplier and recipient, invoice number in the series prescribed at registration, date, place of supply, HSN code per line, taxable value per line, rate of CGST and SGST (or IGST for inter-state), total tax amount, and grand total. Missing the place of supply is the most common omission we see in invoices generated by off-the-shelf SaaS platforms. Without it, the accountant cannot determine whether CGST/SGST or IGST should have been applied.
HSN codes are mandatory for businesses with turnover above 5 crore. Below that, a four-digit code suffices. Above 5 crore, you need at least six digits. We default to six digits regardless because the accountant does not want to revisit this when the client crosses the threshold.
The invoice number series is not something to generate with a UUID or an auto-increment. It has to follow the format registered at the time of GST registration, typically a prefix followed by a zero-padded sequence. If the client registered with `INV/2024-25/001`, your system must continue that exact pattern. We store the prefix and the financial year separately and compute the sequence from a counter table with a row lock at invoice creation time.
For inter-state sales, the tax column should show IGST at the combined rate, not CGST and SGST split. This sounds obvious, but we have seen systems that always split CGST and SGST and then add a note for inter-state transactions. The accountant has to reject that because GSTR-1 expects IGST in its own column.
We also include a small line at the bottom: "Rounded off under Section 170 of CGST Act." Accountants have told us this saves them from a query during audit. It is a line of text that costs nothing to add.
The grand total rounding happens last, after all line items and their taxes are summed:
def invoice_total(lines):
subtotal = sum(l['taxable_value'] for l in lines)
total_tax = sum(l['cgst'] + l['sgst'] for l in lines)
grand = subtotal + total_tax
return grand.quantize(Decimal('1'), rounding=ROUND_HALF_UP)The difference between the unrounded grand total and the rounded one goes into a "rounding adjustment" line. Some accountants want it positive or negative, never zero — if it is zero, they prefer the line omitted. We make that a configuration flag.
We learned most of this from a two-hour call with a client's CA in Noida. The invoice format we shipped after that call has not required changes across six e-commerce projects since.