Skip to content

feat: scan printed labels back into InvenTree, and print its short code on them - #10

Merged
impuls42 merged 1 commit into
mainfrom
feat/sngn-barcode-scan
Sep 25, 2026
Merged

impuls42 merged 1 commit into
mainfrom
feat/sngn-barcode-scan

Conversation

@impuls42

Copy link
Copy Markdown
Member

InvenTree's scanner answers "No match found" for every label this driver prints. The QR holds the agent's short link (https://sngn.top/i/SI1), which a phone opens fine, but InvenTree only resolves its own short format (INV-SI1). Linking barcodes item by item worked for one label and does not scale, so this fixes it on the InvenTree side instead.

Scanning

LabelfabShortlink is a BarcodeMixin plugin registered next to the machine driver. It strips the configured base, rebuilds INV-SI1 and hands that to the builtin InvenTreeBarcode plugin's scan(). So there's no model table here to drift: codes, permissions and the response shape all come from InvenTree. It needs one setting, Short link base, set to the agent's render.qr_base_url. Empty means inert.

Scheme and case are ignored when matching. That matters because two labels are already out in the world as SNGN.TOP/I/PA39 (bare, uppercase) from before the agent switched to https://sngn.top/i/, and both forms should resolve.

The parsing sits in shortlink.py with no InvenTree imports, same split as dispatch/extractors vs driver, so it's unit-tested. barcode.py is the thin shim.

Label text

The title now leads with the short barcode and IPN, with the name underneath:

before:  A1533 | iPhone 5s     after:  INV-SI1 · A1533
         1.00000                       iPhone 5s

INV-SI1 can be typed into InvenTree's scanner as is, so a label still works when its QR doesn't. The prefix is read from the instance's SHORT_BARCODE_PREFIX once per batch. Parts use name rather than full_name, since full_name is already "IPN | name" and printed the IPN twice. Stock items drop the location (it goes stale the moment something moves) and only print a quantity for bulk stock, without Decimal's trailing zeros.

Checked

  • 87 tests pass on Python 3.12 and on a real 3.9 (python:3.9-slim), since CI only runs 3.12 and the package claims 3.9. str.removeprefix is the newest thing used and landed in 3.9.
  • Mutation check: with the startswith guard in parse() disabled, test_rejects[SI1] fails, so the guard is actually covered.
  • Against the live instance: INV-SI1 and INV-PA39 resolve through the builtin today, inv-si1 and SI1 don't. That's why the plugin uppercases and adds the prefix before delegating.

After deploy: activate LabelfabShortlink and set its base to https://sngn.top/i/.

…de on them

Every label this driver prints carries a QR with the agent's short link, e.g.
https://sngn.top/i/SI1. Phones open it fine, but InvenTree's own scanner only knows
its native short format (INV-SI1) and answers "No match found". Linking barcodes one
item at a time is not a fix.

Add LabelfabShortlink, a BarcodeMixin plugin shipped in the same package. It strips
the configured base, rebuilds INV-SI1 and passes it to the builtin InvenTreeBarcode
plugin, so model codes, permissions and the response shape stay InvenTree's. Matching
ignores scheme and case, so labels printed earlier as SNGN.TOP/I/PA39 resolve too. An
empty base keeps the plugin inert.

Label text now leads with the short barcode and IPN ("INV-SI1 · A1533") and puts the
name underneath, so a label whose QR will not scan can still be typed in. The prefix
comes from the instance's SHORT_BARCODE_PREFIX. Part labels use name instead of
full_name, which already embeds the IPN. Stock items drop the location and only show
a quantity for bulk stock, formatted without Decimal's trailing zeros (was 1.00000).
@impuls42
impuls42 merged commit f12eba8 into main Sep 25, 2026
1 check passed
@impuls42
impuls42 deleted the feat/sngn-barcode-scan branch September 25, 2026 14:59
impuls42 added a commit to sengine-cloud/labelfab that referenced this pull request Sep 25, 2026
A title of `INV-SI1` on the stock item preset at 12mm prints as
`INV-SI`. The `1` is past the right edge.

`_wrap` has `if not current or font.getlength(trial) <= width`, so the
first word of each paragraph goes in without being measured. Multi-word
titles get checked from the second word on, which is why this hasn't
shown up before. A single word never is, so `_fit`'s binary search only
has height to satisfy and climbs to `max_pt`. `_fit(120, 60)` on
`INV-SI1` returned one line at 45px, which is exactly 16pt, in a 120px
box.

Measuring the first word fixes that and exposes a second problem.
`_wrap` splits an over-long word at the pixel boundary, so in a box tall
enough for two lines, `INV-S` / `I1` at a big size passes the fit and
beats a smaller size that keeps the word whole. For codes that's worse
than small text, since a split code reads as two codes. `_fit` now runs
the search with word splitting off first, and only falls back to
splitting when nothing fits otherwise. `measure()` also stops reporting
a natural width narrower than the longest word.

The existing tests couldn't catch any of this.
`test_very_long_text_is_truncated_not_overflowed` only asserts the image
size, which is fixed by construction.
`test_text_stays_inside_the_canvas` only checks the top row for ink. The
new tests look at line widths and at the right edge.

Mutation check, same four tests:

- code on main: all 4 fail
- first-word fix only, without the whole-words-first search:
`test_a_word_is_kept_whole_when_a_smaller_size_fits` fails
- this PR: all pass, full suite 275 passed

Found while laying out labels for
sengine-cloud/inventree-label-dispatch#10, which
puts `INV-SI1`-style codes in the title. A part without an IPN gets a
title that is just `INV-PA12`, a single word.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant