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.
- inventree_plugin_explosives-0.1.0/LICENSE +21 -0
- inventree_plugin_explosives-0.1.0/MANIFEST.in +2 -0
- inventree_plugin_explosives-0.1.0/PKG-INFO +252 -0
- inventree_plugin_explosives-0.1.0/README.md +234 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/__init__.py +3 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/constants.py +43 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/core.py +518 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/exports.py +152 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/hazard.py +101 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/neq.py +408 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/parameters.py +279 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/report_templates/magazine_register.html +137 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/report_templates/transport_manifest.html +87 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/serializers.py +69 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/.vite/manifest.json +49 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/Dashboard-Bs2xwbAu.js +2 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/Dashboard-Bs2xwbAu.js.map +1 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/Dashboard.js +2 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/Dashboard.js.map +1 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/LocationPanel-DgH9cN4Z.js +2 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/LocationPanel-DgH9cN4Z.js.map +1 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/LocationPanel.js +2 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/LocationPanel.js.map +1 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/Panel-DVfAY1c4.js +2 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/Panel-DVfAY1c4.js.map +1 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/Panel.js +2 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/Panel.js.map +1 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/Settings-CkxS6q5N.js +2 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/Settings-CkxS6q5N.js.map +1 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/Settings.js +2 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/Settings.js.map +1 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/assets/api-DRKNArex.js +2 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/assets/api-DRKNArex.js.map +1 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/assets/useQuery-bj4Z4fgS.js +125 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/static/assets/useQuery-bj4Z4fgS.js.map +1 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/test_api.py +288 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/test_hazard.py +127 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/test_limits.py +252 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/test_neq.py +395 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/test_parameters.py +123 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/test_ui.py +104 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/test_validation.py +274 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/validation.py +197 -0
- inventree_plugin_explosives-0.1.0/inventree_explosives/views.py +86 -0
- inventree_plugin_explosives-0.1.0/inventree_plugin_explosives.egg-info/PKG-INFO +252 -0
- inventree_plugin_explosives-0.1.0/inventree_plugin_explosives.egg-info/SOURCES.txt +50 -0
- inventree_plugin_explosives-0.1.0/inventree_plugin_explosives.egg-info/dependency_links.txt +1 -0
- inventree_plugin_explosives-0.1.0/inventree_plugin_explosives.egg-info/entry_points.txt +2 -0
- inventree_plugin_explosives-0.1.0/inventree_plugin_explosives.egg-info/top_level.txt +1 -0
- inventree_plugin_explosives-0.1.0/pyproject.toml +48 -0
- 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,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,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"]
|