Skip to content

A bill number a tax invoice may carry - #794

Merged
sridharkalaibala merged 1 commit into
developfrom
fix/a-bill-number-a-tax-invoice-may-carry
Sep 16, 2026
Merged

sridharkalaibala merged 1 commit into
developfrom
fix/a-bill-number-a-tax-invoice-may-carry

Conversation

@sridharkalaibala

Copy link
Copy Markdown
Contributor

Your ask: "we need year pattern required in the sales bill number example attached
have 26 in the year. and then number increasing would be okay. you suggest per day
increase or year wise reset better tell me international standards"
and "i accept
recommandation and may configurable if people from EU and international. Blocker fix
that. i dont know how its got SB1d14 and all."

SB1D14 first

SB1D14-000051 is S + B1, the branch's code + D14, this till's
gateway-assigned code + the running number. The till code is what stops two tills in
one shop minting the same number; it has been there since bill numbers went atomic.
Nothing was wrong with it - it just has no year in it.

Year, not day

India, CGST Rule 46(b): a consecutive serial number, not exceeding sixteen
characters
, of letters, digits, - and /, unique for a financial year. The
invoice you sent is exactly sixteen - VR26-27VIR006782 - because whoever built that
hit the same wall.

EU VAT Directive 2006/112/EC Art 226(2): a sequential number, no length limit, no
prescribed reset.

So the year and the reset are settings, and the charset is not.

Not per day: a daily reset repeats numbers inside the year unless the date is in the
number, and a date costs six to eight of the sixteen. It also destroys what an auditor
actually uses - one consecutive run per year has visible gaps when something is
cancelled, and 365 short runs do not.

What a shop sets

Settings → beside Sale Prefix:

Bill number year What a bill looks like
One run of numbers, no year (default) SB1D14-000051
Start again each financial year SB1D14-27-000051
Start again each calendar year SB1D14-26-000051

Plus the month a financial year starts in - April in India, January in much of the EU.
The month names come from the browser's own calendar rather than from 17 translation
packs.

Off by default. Ninety shops are mid-year with a running series on their invoices.
Switching them on an upgrade would change the shape of every number overnight and
restart the count in the middle of a year, which is the one thing an auditor reads a
series for. Turn it on per shop, best on the first day of that shop's year.

The three things that were hard

Whose midnight. A bill rung up at 00:30 on 1 April in Chennai belongs to the new
financial year. A cloud instance running in UTC would still call it March and number it
into a year that closed half an hour ago. The period is worked out in the shop's own
clock.

The roll-over is one atomic step. A read, a compare and a write would be three, and
a year turns over at midnight in a restaurant that is still serving: two tills billing
in that second would both read the old year and both be given number one. It is a
single pipeline update on the counter row, checked against a real mongod rather than a
mock of one. A counter written before this change reads as "no period" rather than as a
mismatch - otherwise the first bill on every shop in the estate would restart at one.

Sixteen characters is a ceiling, not a guideline. When something has to give it is
the padding on the running number - never the year, which uniqueness depends on, and
never the till code, without which two tills collide. The shop is told, in words it can
act on.

A real bug found on the way

sale.service.js - the counter sale, which is most of the bills in the estate - took a
number from the counter and formatted it itself, while the customer's ordering page
called generateSalesIdForBranch. Both draw on the same counter.

A shop turning the year on would have got the year on its online orders and not on its
counter bills - and the moment the reset put that counter back to one, the till would
have started reissuing numbers it had already given out, caught only by the unique index,
as a failed sale in front of a customer. Both now come through one door, and a test says
so.

Writing the pinning test also found that the unused isReturn path produced
R-SB12D999-000001 - seventeen characters, not a number a tax invoice may carry. It is
now fifteen and legal.

Checks

  • api: 373 suites, 10,800 tests (17 of them against a real mongod)
  • tests/: the settings chain, all five links, plus i18n 50/50
  • npm run check: 7/7
  • The new settings-chain test was run with the branch projection removed and fails there
    with "bill_number_reset is stored but never read back" - the silent failure this whole
    area is prone to.

One thing to tell me

The two-character year is the year the financial year ends in, which is how India
says it (FY27 = April 2026 to March 2027). Your reference bill wrote 26-27, both
years, using five of its sixteen characters. If you would rather see 26 or 26-27,
say which and it is a small change - I picked the convention over the sample.

Owner: "we need year pattern required in the sales bill number example
attached have 26 in the year. and then number increasing would be okay.
you suggest per day increase or year wise reset better tell me
international standards." And on the answer: "i accept recommandation and
may configurable if people from EU and international. Blocker fix that. i
dont know how its got SB1d14 and all."

SB1D14-000051 is S, the branch's code, this till's gateway-assigned code,
and a running number. The till code is what stops two tills in one shop
minting the same number; it has been there since bill numbers went atomic.

THE RULE. India, CGST Rule 46(b): a consecutive serial number, NOT
EXCEEDING SIXTEEN CHARACTERS, of letters, digits, "-" and "/", UNIQUE FOR
A FINANCIAL YEAR. The reference invoice he sent is exactly sixteen -
VR26-27VIR006782 - because whoever built that hit the same wall. The EU
VAT Directive asks only for a sequential number, with no length limit and
no prescribed reset, so the year and the reset are settings and the
charset is not.

Not per day. A daily reset repeats numbers inside the year unless the
date is in the number, and a date costs six to eight of the sixteen. It
also destroys what an auditor actually uses: one consecutive run per year
has visible gaps when something is cancelled, and three hundred and
sixty-five short runs do not.

utils/bill-number.js builds the number and shortens the RUNNING NUMBER
when something has to give - never the year, which uniqueness depends on,
and never the till code, without which two tills collide. With no year
asked for it produces exactly what the template literal it replaced
produced, pinned over 72 combinations.

WHOSE MIDNIGHT. A bill rung up at 00:30 on 1 April in Chennai belongs to
the new financial year. A cloud instance running in UTC would still call
it March and number it into a year that closed half an hour ago. The
period is worked out in the shop's own clock.

THE ROLL-OVER IS ONE ATOMIC STEP. A read, a compare and a write would be
three, and a year turns over at midnight in a restaurant that is still
serving. One pipeline update on the counter row, so two tills billing in
that second cannot both be given number one. Checked against a real
mongod, not a mock of one. A counter written before any of this reads as
"no period" rather than as a mismatch, or the first bill on every shop in
the estate would restart at one.

And it found a real one: the counter sale formatted its own number while
the customer's ordering page called generateSalesIdForBranch, with both
drawing on one counter. A shop turning the year on would have had it on
its online orders and not on its counter bills, and the reset would have
had the till reissuing numbers it had already given out. Both now come
through one door.

Off by default. Ninety shops are mid-year with a running series on their
invoices; switching them on an upgrade would change the shape of every
number overnight and restart the count in the middle of a year. A shop
turns it on, best on the first day of its own year.
@sridharkalaibala
sridharkalaibala merged commit 4e39930 into develop Sep 16, 2026
10 checks passed
@github-actions github-actions Bot added the ready for QA Merged to develop and live on develop.posnic.io - anyone can test it label Sep 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Merged to develop. Anyone can test this - you do not need write access.

Try it at https://develop.posnic.io, or run it yourself:

git fetch origin develop && git checkout develop
npm install && npm --prefix api install
npm run dev   # then http://localhost:3000

When you have tested it, say what you did and what happened, and set
QA passed or QA failed. If you cannot set labels, just comment -
a maintainer will.

Reporting that something is broken is as useful as fixing it. It is
better found here than by a shopkeeper.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for QA Merged to develop and live on develop.posnic.io - anyone can test it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant