inventree-plugin-explosives 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. inventree_plugin_explosives-0.1.0/LICENSE +21 -0
  2. inventree_plugin_explosives-0.1.0/MANIFEST.in +2 -0
  3. inventree_plugin_explosives-0.1.0/PKG-INFO +252 -0
  4. inventree_plugin_explosives-0.1.0/README.md +234 -0
  5. inventree_plugin_explosives-0.1.0/inventree_explosives/__init__.py +3 -0
  6. inventree_plugin_explosives-0.1.0/inventree_explosives/constants.py +43 -0
  7. inventree_plugin_explosives-0.1.0/inventree_explosives/core.py +518 -0
  8. inventree_plugin_explosives-0.1.0/inventree_explosives/exports.py +152 -0
  9. inventree_plugin_explosives-0.1.0/inventree_explosives/hazard.py +101 -0
  10. inventree_plugin_explosives-0.1.0/inventree_explosives/neq.py +408 -0
  11. inventree_plugin_explosives-0.1.0/inventree_explosives/parameters.py +279 -0
  12. inventree_plugin_explosives-0.1.0/inventree_explosives/report_templates/magazine_register.html +137 -0
  13. inventree_plugin_explosives-0.1.0/inventree_explosives/report_templates/transport_manifest.html +87 -0
  14. inventree_plugin_explosives-0.1.0/inventree_explosives/serializers.py +69 -0
  15. inventree_plugin_explosives-0.1.0/inventree_explosives/static/.vite/manifest.json +49 -0
  16. inventree_plugin_explosives-0.1.0/inventree_explosives/static/Dashboard-Bs2xwbAu.js +2 -0
  17. inventree_plugin_explosives-0.1.0/inventree_explosives/static/Dashboard-Bs2xwbAu.js.map +1 -0
  18. inventree_plugin_explosives-0.1.0/inventree_explosives/static/Dashboard.js +2 -0
  19. inventree_plugin_explosives-0.1.0/inventree_explosives/static/Dashboard.js.map +1 -0
  20. inventree_plugin_explosives-0.1.0/inventree_explosives/static/LocationPanel-DgH9cN4Z.js +2 -0
  21. inventree_plugin_explosives-0.1.0/inventree_explosives/static/LocationPanel-DgH9cN4Z.js.map +1 -0
  22. inventree_plugin_explosives-0.1.0/inventree_explosives/static/LocationPanel.js +2 -0
  23. inventree_plugin_explosives-0.1.0/inventree_explosives/static/LocationPanel.js.map +1 -0
  24. inventree_plugin_explosives-0.1.0/inventree_explosives/static/Panel-DVfAY1c4.js +2 -0
  25. inventree_plugin_explosives-0.1.0/inventree_explosives/static/Panel-DVfAY1c4.js.map +1 -0
  26. inventree_plugin_explosives-0.1.0/inventree_explosives/static/Panel.js +2 -0
  27. inventree_plugin_explosives-0.1.0/inventree_explosives/static/Panel.js.map +1 -0
  28. inventree_plugin_explosives-0.1.0/inventree_explosives/static/Settings-CkxS6q5N.js +2 -0
  29. inventree_plugin_explosives-0.1.0/inventree_explosives/static/Settings-CkxS6q5N.js.map +1 -0
  30. inventree_plugin_explosives-0.1.0/inventree_explosives/static/Settings.js +2 -0
  31. inventree_plugin_explosives-0.1.0/inventree_explosives/static/Settings.js.map +1 -0
  32. inventree_plugin_explosives-0.1.0/inventree_explosives/static/assets/api-DRKNArex.js +2 -0
  33. inventree_plugin_explosives-0.1.0/inventree_explosives/static/assets/api-DRKNArex.js.map +1 -0
  34. inventree_plugin_explosives-0.1.0/inventree_explosives/static/assets/useQuery-bj4Z4fgS.js +125 -0
  35. inventree_plugin_explosives-0.1.0/inventree_explosives/static/assets/useQuery-bj4Z4fgS.js.map +1 -0
  36. inventree_plugin_explosives-0.1.0/inventree_explosives/test_api.py +288 -0
  37. inventree_plugin_explosives-0.1.0/inventree_explosives/test_hazard.py +127 -0
  38. inventree_plugin_explosives-0.1.0/inventree_explosives/test_limits.py +252 -0
  39. inventree_plugin_explosives-0.1.0/inventree_explosives/test_neq.py +395 -0
  40. inventree_plugin_explosives-0.1.0/inventree_explosives/test_parameters.py +123 -0
  41. inventree_plugin_explosives-0.1.0/inventree_explosives/test_ui.py +104 -0
  42. inventree_plugin_explosives-0.1.0/inventree_explosives/test_validation.py +274 -0
  43. inventree_plugin_explosives-0.1.0/inventree_explosives/validation.py +197 -0
  44. inventree_plugin_explosives-0.1.0/inventree_explosives/views.py +86 -0
  45. inventree_plugin_explosives-0.1.0/inventree_plugin_explosives.egg-info/PKG-INFO +252 -0
  46. inventree_plugin_explosives-0.1.0/inventree_plugin_explosives.egg-info/SOURCES.txt +50 -0
  47. inventree_plugin_explosives-0.1.0/inventree_plugin_explosives.egg-info/dependency_links.txt +1 -0
  48. inventree_plugin_explosives-0.1.0/inventree_plugin_explosives.egg-info/entry_points.txt +2 -0
  49. inventree_plugin_explosives-0.1.0/inventree_plugin_explosives.egg-info/top_level.txt +1 -0
  50. inventree_plugin_explosives-0.1.0/pyproject.toml +48 -0
  51. inventree_plugin_explosives-0.1.0/setup.cfg +19 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sinn Development Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ recursive-include inventree_explosives/static *
2
+ recursive-include inventree_explosives/report_templates *
@@ -0,0 +1,252 @@
1
+ Metadata-Version: 2.4
2
+ Name: inventree-plugin-explosives
3
+ Version: 0.1.0
4
+ Summary: Explosives inventory management for InvenTree: net explosive quantity, gross mass, UN classification and magazine licence limits
5
+ Author-email: Sinn Development Ltd <engineering@sinndevelopment.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/sinndevelopment/inventree-plugin-explosives
8
+ Project-URL: Repository, https://github.com/sinndevelopment/inventree-plugin-explosives
9
+ Project-URL: Issues, https://github.com/sinndevelopment/inventree-plugin-explosives/issues
10
+ Keywords: inventree,plugin,explosives,neq,adr,compliance
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Framework :: InvenTree
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ # InvenTree Explosives Plugin
20
+
21
+ Adds the properties needed to keep a lawful inventory of explosives in
22
+ [InvenTree](https://inventree.org): **net explosive quantity (NEQ)**, **gross
23
+ mass**, and **UN classification** — and enforces the **licensed NEQ limit** of
24
+ each magazine.
25
+
26
+ The point is not just to record fields. A licensed magazine may only hold so much
27
+ net explosive quantity, and ordinary stock movements can breach that limit. This
28
+ plugin aggregates NEQ per storage location, compares it against the licence, and
29
+ warns (or blocks) when a movement would take it over.
30
+
31
+ ## What it does
32
+
33
+ - **Part properties** — NEQ per unit, gross mass per unit, UN hazard division,
34
+ compatibility group, UN number, proper shipping name.
35
+ - **Magazine totals** — total NEQ held in a stock location (including
36
+ sublocations), against a per-location licensed limit, with a utilisation ring
37
+ and an alert when exceeded.
38
+ - **Validation** — rejects explosive parts with no NEQ, an NEQ greater than the
39
+ gross mass, invalid UN numbers, and illegal classification codes (`1.1S` is not
40
+ a thing; `1.4S` is).
41
+ - **Compliance output** — a magazine register and a transport-manifest report,
42
+ plus explosive columns on the standard Part / Stock / Location CSV exports.
43
+
44
+ ## How the data is modelled
45
+
46
+ Everything is stored as **native InvenTree parameters** — no custom database
47
+ tables, no migrations, nothing to break on upgrade.
48
+
49
+ | Field | Lives on | Units |
50
+ |----------------------------------|--------------------|---------------|
51
+ | `Explosive` (checkbox) | Part | — |
52
+ | `Net Explosive Quantity` | Part | kg |
53
+ | `Explosive Gross Mass` | Part | kg |
54
+ | `UN Hazard Division` | Part | 1.1 – 1.6 |
55
+ | `UN Compatibility Group` | Part | A – S |
56
+ | `UN Number` | Part | e.g. `UN0241` |
57
+ | `Proper Shipping Name` | Part | — |
58
+ | `Maximum Net Explosive Quantity` | **Stock Location** | kg |
59
+
60
+ NEQ is recorded **per unit** on the Part. A magazine's total is computed as
61
+ `Σ (stock quantity × NEQ per unit)`.
62
+
63
+ This means two lots of the same part are assumed to have the same NEQ per unit.
64
+ That is true of manufactured explosive articles, which is the common case. If you
65
+ need per-lot NEQ (a weighed actual mass at receipt, say), that is not supported
66
+ in this version — InvenTree does not currently support parameters on stock items
67
+ ([upstream PR #11459](https://github.com/inventree/InvenTree/pull/11459) is
68
+ deferred).
69
+
70
+ Because the parameters are unit-aware, entering `500 g` into a kg field stores
71
+ `0.5`. You can enter mass in whatever unit is convenient.
72
+
73
+ ## Installation
74
+
75
+ ```bash
76
+ pip install inventree-plugin-explosives
77
+ ```
78
+
79
+ Then in InvenTree:
80
+
81
+ 1. Enable the plugin (Admin Center → Plugins).
82
+ 2. Enable the global settings **`ENABLE_PLUGINS_URL`** (for the API) and
83
+ **`ENABLE_PLUGINS_INTERFACE`** (for the panels). Both are off by default and
84
+ the plugin's UI will not work without them.
85
+ 3. The parameter templates are created automatically. If they are missing (e.g.
86
+ the plugin first loaded during a migration), open the plugin settings and
87
+ press **Run setup**.
88
+
89
+ Requires InvenTree **1.0.0+** (tested against 1.4.1).
90
+
91
+ ## Usage
92
+
93
+ 1. Tick **Explosive** on a part, and set its NEQ, gross mass and UN
94
+ classification on the Parameters tab.
95
+ 2. Set **Maximum Net Explosive Quantity** on the stock location that is your
96
+ licensed magazine.
97
+ 3. The **Explosives / NEQ** panel on that location shows what is held against the
98
+ licence. The dashboard item lists every licensed magazine by utilisation.
99
+
100
+ ### Licence limit enforcement
101
+
102
+ The `LIMIT_ACTION` setting controls what happens when a stock movement would take
103
+ a magazine over its licensed NEQ:
104
+
105
+ - **`warn`** (default) — the movement proceeds; the panel, the dashboard and a
106
+ notification report the breach.
107
+ - **`block`** — the movement is rejected with a validation error.
108
+ - **`off`** — no checking.
109
+
110
+ It ships as `warn` on purpose. The check runs on every stock save, including
111
+ purchase-order receipts, build outputs and stocktakes, and a hard failure inside
112
+ one of those workflows is opaque and disruptive. Run in `warn` until you trust
113
+ the numbers against your real data, then switch to `block`.
114
+
115
+ **`block` is a strong deterrent, not a guarantee.** Bulk operations that use
116
+ `QuerySet.update()` bypass Django's `save()` entirely and cannot be intercepted.
117
+ The plugin also audits locations on stock events and raises a notification after
118
+ the fact, but a licence holder should not treat this plugin as making a breach
119
+ *impossible*.
120
+
121
+ A magazine's licence covers **everything stored beneath it**, not just stock
122
+ placed in the location directly. Putting explosives in a sublocation of a
123
+ licensed magazine counts against that magazine's limit (unless
124
+ `INCLUDE_SUBLOCATIONS` is off).
125
+
126
+ A limit of **0 kg** means "no explosives permitted here" and is enforced as such
127
+ — it is not the same as leaving the limit unset, which means "unlicensed, not
128
+ tracked".
129
+
130
+ ### What counts toward the total
131
+
132
+ By default, **all physically present stock counts**, including stock that is
133
+ `QUARANTINED` or `REJECTED` — such stock is still sitting in the magazine and
134
+ still counts against the licence. Only `DESTROYED` and `LOST` stock is excluded.
135
+ Set `COUNT_ALL_PRESENT_STOCK` to false to fall back to InvenTree's ordinary
136
+ "available stock" semantics.
137
+
138
+ ## Reports
139
+
140
+ Two templates ship in `inventree_explosives/report_templates/`. InvenTree has no
141
+ hook to install report templates automatically, so upload them yourself under
142
+ Admin Center → Reports:
143
+
144
+ - `magazine_register.html` — model **Stock Location**. What is held, against the
145
+ licence, broken down by hazard division.
146
+ - `transport_manifest.html` — model **Stock Item**. The dangerous-goods
147
+ description for a consignment.
148
+
149
+ Both read from a single `explosives` context key, so custom templates can use the
150
+ same data: `{{ explosives.neq_kg }}`, `{{ explosives.items }}`,
151
+ `{{ explosives.classification_code }}`, and so on.
152
+
153
+ ## ⚠️ Regulatory sign-off
154
+
155
+ The UN classification table in `hazard.py` encodes the 35 legal
156
+ division/compatibility-group combinations. It has been transcribed against the UN
157
+ Model Regulations / ADR 2.2.1.1.4 classification-code table and is verified by an
158
+ exhaustive 78-cell test.
159
+
160
+ **Before relying on this in a regulated setting, confirm the table against the
161
+ ADR/IMDG edition your licence is issued under, and record that edition here.**
162
+
163
+ > Classification table checked against: _(record your edition here)_
164
+
165
+ This plugin is an inventory aid. It does not discharge any duty of the licence
166
+ holder, the consignor, or the responsible person.
167
+
168
+ ## Development
169
+
170
+ ```bash
171
+ # this plugin
172
+ git clone https://github.com/sinndevelopment/inventree-plugin-explosives \
173
+ ~/git/inventree-plugin-explosives
174
+
175
+ # InvenTree checkout to develop against
176
+ git clone https://github.com/inventree/InvenTree ~/git/InvenTree
177
+ cd ~/git/InvenTree && git checkout 1.4.1
178
+ python3.12 -m venv .venv && source .venv/bin/activate
179
+ pip install -U invoke && invoke install && invoke dev.setup-dev
180
+
181
+ # the plugin, editable
182
+ pip install -e ~/git/inventree-plugin-explosives
183
+ ```
184
+
185
+ Build the frontend before packaging, or the wheel ships an empty `static/` and
186
+ every panel silently fails to render:
187
+
188
+ ```bash
189
+ cd frontend && npm install && npm run build
190
+ ```
191
+
192
+ For frontend hot reload, set `DEBUG=True`, `PLUGIN_DEV_SLUG=explosives` and
193
+ `PLUGIN_DEV_HOST=http://localhost:5173`, then run `npm run dev`.
194
+
195
+ ### Releasing
196
+
197
+ Publishing to PyPI happens automatically when a GitHub release is published, via
198
+ the `publish.yml` workflow. It uses **trusted publishing (OIDC)** — there is no API
199
+ token to store or rotate.
200
+
201
+ Before the first release, register the trusted publisher on PyPI (Project →
202
+ Settings → Publishing, or as a *pending publisher* if the project does not exist
203
+ there yet):
204
+
205
+ | Field | Value |
206
+ |---|---|
207
+ | Owner | `sinndevelopment` |
208
+ | Repository | `inventree-plugin-explosives` |
209
+ | Workflow | `publish.yml` |
210
+ | Environment | `pypi` |
211
+
212
+ Then create a `pypi` environment under repository Settings → Environments. Any
213
+ protection rules on it (required reviewers, tag restrictions) gate every release.
214
+
215
+ The workflow builds the frontend and refuses to publish a wheel that does not
216
+ contain the compiled panels. The full test suite runs first, against a real
217
+ InvenTree, and a failure blocks the release.
218
+
219
+ ### Continuous integration
220
+
221
+ | Workflow | When | What |
222
+ |---|---|---|
223
+ | `ci.yaml` | every push and PR | ruff, biome, package build, frontend build, and the standalone classification-table tests |
224
+ | `test.yaml` | pushes to `main`, and before every release | the full suite against a real InvenTree checkout |
225
+ | `publish.yml` | published release | full suite → build → publish via OIDC |
226
+
227
+ `test.yaml` is a reusable workflow. It pins the InvenTree version it tests
228
+ against (currently 1.4.1); bump the `inventree-ref` default when upgrading, or
229
+ run it manually against another ref from the Actions tab.
230
+
231
+ ### Tests
232
+
233
+ The pure classification logic runs standalone:
234
+
235
+ ```bash
236
+ python -m unittest inventree_explosives.test_hazard -v
237
+ ```
238
+
239
+ The rest need an InvenTree checkout:
240
+
241
+ ```bash
242
+ cd ~/git/InvenTree
243
+ export INVENTREE_PLUGINS_ENABLED=true INVENTREE_PLUGIN_TESTING=true \
244
+ INVENTREE_PLUGIN_TESTING_SETUP=true
245
+ python src/backend/InvenTree/manage.py test inventree_explosives
246
+ ```
247
+
248
+ ## Licence
249
+
250
+ MIT — Copyright (c) 2026 Sinn Development Ltd. See [LICENSE](LICENSE).
251
+
252
+ Source: <https://github.com/sinndevelopment/inventree-plugin-explosives>
@@ -0,0 +1,234 @@
1
+ # InvenTree Explosives Plugin
2
+
3
+ Adds the properties needed to keep a lawful inventory of explosives in
4
+ [InvenTree](https://inventree.org): **net explosive quantity (NEQ)**, **gross
5
+ mass**, and **UN classification** — and enforces the **licensed NEQ limit** of
6
+ each magazine.
7
+
8
+ The point is not just to record fields. A licensed magazine may only hold so much
9
+ net explosive quantity, and ordinary stock movements can breach that limit. This
10
+ plugin aggregates NEQ per storage location, compares it against the licence, and
11
+ warns (or blocks) when a movement would take it over.
12
+
13
+ ## What it does
14
+
15
+ - **Part properties** — NEQ per unit, gross mass per unit, UN hazard division,
16
+ compatibility group, UN number, proper shipping name.
17
+ - **Magazine totals** — total NEQ held in a stock location (including
18
+ sublocations), against a per-location licensed limit, with a utilisation ring
19
+ and an alert when exceeded.
20
+ - **Validation** — rejects explosive parts with no NEQ, an NEQ greater than the
21
+ gross mass, invalid UN numbers, and illegal classification codes (`1.1S` is not
22
+ a thing; `1.4S` is).
23
+ - **Compliance output** — a magazine register and a transport-manifest report,
24
+ plus explosive columns on the standard Part / Stock / Location CSV exports.
25
+
26
+ ## How the data is modelled
27
+
28
+ Everything is stored as **native InvenTree parameters** — no custom database
29
+ tables, no migrations, nothing to break on upgrade.
30
+
31
+ | Field | Lives on | Units |
32
+ |----------------------------------|--------------------|---------------|
33
+ | `Explosive` (checkbox) | Part | — |
34
+ | `Net Explosive Quantity` | Part | kg |
35
+ | `Explosive Gross Mass` | Part | kg |
36
+ | `UN Hazard Division` | Part | 1.1 – 1.6 |
37
+ | `UN Compatibility Group` | Part | A – S |
38
+ | `UN Number` | Part | e.g. `UN0241` |
39
+ | `Proper Shipping Name` | Part | — |
40
+ | `Maximum Net Explosive Quantity` | **Stock Location** | kg |
41
+
42
+ NEQ is recorded **per unit** on the Part. A magazine's total is computed as
43
+ `Σ (stock quantity × NEQ per unit)`.
44
+
45
+ This means two lots of the same part are assumed to have the same NEQ per unit.
46
+ That is true of manufactured explosive articles, which is the common case. If you
47
+ need per-lot NEQ (a weighed actual mass at receipt, say), that is not supported
48
+ in this version — InvenTree does not currently support parameters on stock items
49
+ ([upstream PR #11459](https://github.com/inventree/InvenTree/pull/11459) is
50
+ deferred).
51
+
52
+ Because the parameters are unit-aware, entering `500 g` into a kg field stores
53
+ `0.5`. You can enter mass in whatever unit is convenient.
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ pip install inventree-plugin-explosives
59
+ ```
60
+
61
+ Then in InvenTree:
62
+
63
+ 1. Enable the plugin (Admin Center → Plugins).
64
+ 2. Enable the global settings **`ENABLE_PLUGINS_URL`** (for the API) and
65
+ **`ENABLE_PLUGINS_INTERFACE`** (for the panels). Both are off by default and
66
+ the plugin's UI will not work without them.
67
+ 3. The parameter templates are created automatically. If they are missing (e.g.
68
+ the plugin first loaded during a migration), open the plugin settings and
69
+ press **Run setup**.
70
+
71
+ Requires InvenTree **1.0.0+** (tested against 1.4.1).
72
+
73
+ ## Usage
74
+
75
+ 1. Tick **Explosive** on a part, and set its NEQ, gross mass and UN
76
+ classification on the Parameters tab.
77
+ 2. Set **Maximum Net Explosive Quantity** on the stock location that is your
78
+ licensed magazine.
79
+ 3. The **Explosives / NEQ** panel on that location shows what is held against the
80
+ licence. The dashboard item lists every licensed magazine by utilisation.
81
+
82
+ ### Licence limit enforcement
83
+
84
+ The `LIMIT_ACTION` setting controls what happens when a stock movement would take
85
+ a magazine over its licensed NEQ:
86
+
87
+ - **`warn`** (default) — the movement proceeds; the panel, the dashboard and a
88
+ notification report the breach.
89
+ - **`block`** — the movement is rejected with a validation error.
90
+ - **`off`** — no checking.
91
+
92
+ It ships as `warn` on purpose. The check runs on every stock save, including
93
+ purchase-order receipts, build outputs and stocktakes, and a hard failure inside
94
+ one of those workflows is opaque and disruptive. Run in `warn` until you trust
95
+ the numbers against your real data, then switch to `block`.
96
+
97
+ **`block` is a strong deterrent, not a guarantee.** Bulk operations that use
98
+ `QuerySet.update()` bypass Django's `save()` entirely and cannot be intercepted.
99
+ The plugin also audits locations on stock events and raises a notification after
100
+ the fact, but a licence holder should not treat this plugin as making a breach
101
+ *impossible*.
102
+
103
+ A magazine's licence covers **everything stored beneath it**, not just stock
104
+ placed in the location directly. Putting explosives in a sublocation of a
105
+ licensed magazine counts against that magazine's limit (unless
106
+ `INCLUDE_SUBLOCATIONS` is off).
107
+
108
+ A limit of **0 kg** means "no explosives permitted here" and is enforced as such
109
+ — it is not the same as leaving the limit unset, which means "unlicensed, not
110
+ tracked".
111
+
112
+ ### What counts toward the total
113
+
114
+ By default, **all physically present stock counts**, including stock that is
115
+ `QUARANTINED` or `REJECTED` — such stock is still sitting in the magazine and
116
+ still counts against the licence. Only `DESTROYED` and `LOST` stock is excluded.
117
+ Set `COUNT_ALL_PRESENT_STOCK` to false to fall back to InvenTree's ordinary
118
+ "available stock" semantics.
119
+
120
+ ## Reports
121
+
122
+ Two templates ship in `inventree_explosives/report_templates/`. InvenTree has no
123
+ hook to install report templates automatically, so upload them yourself under
124
+ Admin Center → Reports:
125
+
126
+ - `magazine_register.html` — model **Stock Location**. What is held, against the
127
+ licence, broken down by hazard division.
128
+ - `transport_manifest.html` — model **Stock Item**. The dangerous-goods
129
+ description for a consignment.
130
+
131
+ Both read from a single `explosives` context key, so custom templates can use the
132
+ same data: `{{ explosives.neq_kg }}`, `{{ explosives.items }}`,
133
+ `{{ explosives.classification_code }}`, and so on.
134
+
135
+ ## ⚠️ Regulatory sign-off
136
+
137
+ The UN classification table in `hazard.py` encodes the 35 legal
138
+ division/compatibility-group combinations. It has been transcribed against the UN
139
+ Model Regulations / ADR 2.2.1.1.4 classification-code table and is verified by an
140
+ exhaustive 78-cell test.
141
+
142
+ **Before relying on this in a regulated setting, confirm the table against the
143
+ ADR/IMDG edition your licence is issued under, and record that edition here.**
144
+
145
+ > Classification table checked against: _(record your edition here)_
146
+
147
+ This plugin is an inventory aid. It does not discharge any duty of the licence
148
+ holder, the consignor, or the responsible person.
149
+
150
+ ## Development
151
+
152
+ ```bash
153
+ # this plugin
154
+ git clone https://github.com/sinndevelopment/inventree-plugin-explosives \
155
+ ~/git/inventree-plugin-explosives
156
+
157
+ # InvenTree checkout to develop against
158
+ git clone https://github.com/inventree/InvenTree ~/git/InvenTree
159
+ cd ~/git/InvenTree && git checkout 1.4.1
160
+ python3.12 -m venv .venv && source .venv/bin/activate
161
+ pip install -U invoke && invoke install && invoke dev.setup-dev
162
+
163
+ # the plugin, editable
164
+ pip install -e ~/git/inventree-plugin-explosives
165
+ ```
166
+
167
+ Build the frontend before packaging, or the wheel ships an empty `static/` and
168
+ every panel silently fails to render:
169
+
170
+ ```bash
171
+ cd frontend && npm install && npm run build
172
+ ```
173
+
174
+ For frontend hot reload, set `DEBUG=True`, `PLUGIN_DEV_SLUG=explosives` and
175
+ `PLUGIN_DEV_HOST=http://localhost:5173`, then run `npm run dev`.
176
+
177
+ ### Releasing
178
+
179
+ Publishing to PyPI happens automatically when a GitHub release is published, via
180
+ the `publish.yml` workflow. It uses **trusted publishing (OIDC)** — there is no API
181
+ token to store or rotate.
182
+
183
+ Before the first release, register the trusted publisher on PyPI (Project →
184
+ Settings → Publishing, or as a *pending publisher* if the project does not exist
185
+ there yet):
186
+
187
+ | Field | Value |
188
+ |---|---|
189
+ | Owner | `sinndevelopment` |
190
+ | Repository | `inventree-plugin-explosives` |
191
+ | Workflow | `publish.yml` |
192
+ | Environment | `pypi` |
193
+
194
+ Then create a `pypi` environment under repository Settings → Environments. Any
195
+ protection rules on it (required reviewers, tag restrictions) gate every release.
196
+
197
+ The workflow builds the frontend and refuses to publish a wheel that does not
198
+ contain the compiled panels. The full test suite runs first, against a real
199
+ InvenTree, and a failure blocks the release.
200
+
201
+ ### Continuous integration
202
+
203
+ | Workflow | When | What |
204
+ |---|---|---|
205
+ | `ci.yaml` | every push and PR | ruff, biome, package build, frontend build, and the standalone classification-table tests |
206
+ | `test.yaml` | pushes to `main`, and before every release | the full suite against a real InvenTree checkout |
207
+ | `publish.yml` | published release | full suite → build → publish via OIDC |
208
+
209
+ `test.yaml` is a reusable workflow. It pins the InvenTree version it tests
210
+ against (currently 1.4.1); bump the `inventree-ref` default when upgrading, or
211
+ run it manually against another ref from the Actions tab.
212
+
213
+ ### Tests
214
+
215
+ The pure classification logic runs standalone:
216
+
217
+ ```bash
218
+ python -m unittest inventree_explosives.test_hazard -v
219
+ ```
220
+
221
+ The rest need an InvenTree checkout:
222
+
223
+ ```bash
224
+ cd ~/git/InvenTree
225
+ export INVENTREE_PLUGINS_ENABLED=true INVENTREE_PLUGIN_TESTING=true \
226
+ INVENTREE_PLUGIN_TESTING_SETUP=true
227
+ python src/backend/InvenTree/manage.py test inventree_explosives
228
+ ```
229
+
230
+ ## Licence
231
+
232
+ MIT — Copyright (c) 2026 Sinn Development Ltd. See [LICENSE](LICENSE).
233
+
234
+ Source: <https://github.com/sinndevelopment/inventree-plugin-explosives>
@@ -0,0 +1,3 @@
1
+ """Explosives inventory management plugin for InvenTree."""
2
+
3
+ PLUGIN_VERSION = "0.1.0"
@@ -0,0 +1,43 @@
1
+ """Names and vocabularies used by the explosives plugin.
2
+
3
+ Everything the plugin looks up by name lives here, so that a rename is a
4
+ one-line change rather than a grep.
5
+ """
6
+
7
+ # ParameterTemplate.name is globally unique and case-insensitive across the whole
8
+ # InvenTree instance, so these can collide with templates a site already has.
9
+
10
+ TPL_EXPLOSIVE = "Explosive"
11
+ TPL_NEQ = "Net Explosive Quantity"
12
+ TPL_GROSS_MASS = "Explosive Gross Mass"
13
+ TPL_DIVISION = "UN Hazard Division"
14
+ TPL_COMPAT = "UN Compatibility Group"
15
+ TPL_UN_NUMBER = "UN Number"
16
+ TPL_PSN = "Proper Shipping Name"
17
+
18
+ # Applied to StockLocation, not Part.
19
+ TPL_MAX_NEQ = "Maximum Net Explosive Quantity"
20
+
21
+ PART_TEMPLATES = [
22
+ TPL_EXPLOSIVE,
23
+ TPL_NEQ,
24
+ TPL_GROSS_MASS,
25
+ TPL_DIVISION,
26
+ TPL_COMPAT,
27
+ TPL_UN_NUMBER,
28
+ TPL_PSN,
29
+ ]
30
+
31
+ LOCATION_TEMPLATES = [TPL_MAX_NEQ]
32
+
33
+ TEMPLATE_COUNT_EXPECTED = len(PART_TEMPLATES) + len(LOCATION_TEMPLATES)
34
+
35
+ # Mass templates are declared in kg so that Parameter.data_numeric is itself in
36
+ # kg and can be summed directly; pint converts user input ("500 g" -> 0.5) on the
37
+ # way in. Changing this changes the meaning of every stored value.
38
+ MASS_UNIT = "kg"
39
+
40
+ DIVISIONS = ["1.1", "1.2", "1.3", "1.4", "1.5", "1.6"]
41
+
42
+ # The letters are not contiguous: there is no I, M, O, P, Q or R.
43
+ COMPATIBILITY_GROUPS = ["A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "N", "S"]