Design for a 3D-printed-parts storefront: item catalog with images and quantity on hand, a transaction ledger attributing every take/restock/ adjust to a badge-scanned employee, an unauthenticated touch kiosk (scan bin barcode, scan badge, keypad quantity), 1x0.5in CODE128 bin labels, and stock/consumption/by-person reports. The lab guide walks a developer through building it in seven checkpointed milestones, reusing the USB badge contract, the measuringtools migration baseline, the models-image upload trio, and the open kiosk-endpoint precedents.
210 lines
10 KiB
Markdown
210 lines
10 KiB
Markdown
# Plugin lab: build the printedparts plugin yourself
|
|
|
|
A guided, milestone-based exercise: build the 3D-printed-parts storefront +
|
|
kiosk plugin specified in `docs/proposals/printedparts-plugin.md`. Each
|
|
milestone lists what to build, which existing code to imitate, and a
|
|
checkpoint that proves you are done. Read the spec first, keep it open.
|
|
|
|
Prerequisites: a working dev environment (README quick start), the three
|
|
plugin docs skimmed once - `PLUGIN-QUICKSTART.md` (mechanics),
|
|
`PLUGIN-GUIDE.md` (the measuringtools walkthrough - your narrative reference),
|
|
`PLUGIN-HOOKS.md` (hook reference). Naming rules: `CONTRIBUTING.md` - the
|
|
pre-commit hook enforces them, read it before naming anything.
|
|
|
|
Ground rules
|
|
- Import core ONLY via `shopdb.api` (+ `shopdb.plugins.base`). The contract
|
|
test fails your build otherwise.
|
|
- DB columns: lowercase concatenated (`quantityonhand`, not quantity_on_hand).
|
|
- Run `bash scripts/check-naming-and-style.sh` and the test suite at every
|
|
checkpoint.
|
|
|
|
---
|
|
|
|
## Milestone 1 - skeleton, models, migration (backend exists)
|
|
|
|
Build
|
|
1. `flask plugin new printedparts` - scaffolds `plugins/printedparts/`.
|
|
2. The scaffold assumes an Asset-extension plugin; ours is standalone.
|
|
In `plugin.py` strip the AssetType seeding from `on_install` (imitate
|
|
`plugins/knowledgebase/plugin.py` instead of the template).
|
|
3. Replace the scaffold model with the two spec tables: `PrintedItem`,
|
|
`PrintedItemTransaction` (`models/printeditem.py`). Use `BaseModel` +
|
|
`AuditMixin` from `shopdb.api`. Itemcode: leave generation to the API
|
|
layer (M2), column just `unique=True, index=True`.
|
|
4. Register both tables in `PLUGIN_TABLE_OWNERS`
|
|
(`shopdb/plugins/alembic_template.py`).
|
|
5. Create `plugins/printedparts/migrations/` with the 3-line `env.py` +
|
|
`script.py.mako` (copy from measuringtools) and a REAL baseline
|
|
`versions/0001_printedparts_baseline.py` - hand-written
|
|
`op.create_table(...)` for both tables (see
|
|
`plugins/measuringtools/migrations/versions/0001_measuringtools_baseline.py`).
|
|
6. Manifest: api_prefix `/api/printedparts`, `dependencies: ["employees"]`,
|
|
`default_enabled: false`.
|
|
|
|
Checkpoint
|
|
```
|
|
flask plugin install printedparts && flask plugin enable printedparts
|
|
flask plugin upgrade-all # applies your 0001
|
|
mysql> SHOW TABLES LIKE 'printed%'; -- both tables
|
|
mysql> SELECT * FROM alembic_version_printedparts; -- your revision id
|
|
pytest tests/ -q # nothing broken, contract tests green
|
|
```
|
|
|
|
## Milestone 2 - CRUD API + permissions + itemcode
|
|
|
|
Build
|
|
1. `api/routes.py`: list/detail/create/update/soft-delete per the spec table.
|
|
Imitate a clean plugin blueprint (`plugins/measuringtools/api/routes.py`)
|
|
for pagination (`perpage`, `dir`), search, and the shared
|
|
`success_response`/`error_response` helpers from `shopdb.api`.
|
|
2. Itemcode on create: `<prefix>-<id zero-padded to 4>`; prefix from Setting
|
|
`printedparts_code_prefix` (read via `self.get_setting` or Setting model
|
|
through `shopdb.api`). Two-step: insert, flush to get the id, set code.
|
|
3. `get_permissions()` on the plugin class: view/create/edit/delete/restock
|
|
(tuples, category `printedparts` - copy shape from
|
|
`plugins/usb/plugin.py`). Gate mutations with `@jwt_required()` +
|
|
`@require_permission(...)`; reads are `@jwt_required(optional=True)`.
|
|
4. Restock + adjust endpoints: both write a LEDGER row and move
|
|
`quantityonhand` in the same commit. Adjust requires `reason`, rejects a
|
|
result below zero. Both record the operator: accept `badge` in the body
|
|
and resolve it (M4 extracts the resolver - for now digits=SSO is enough).
|
|
5. Seed the 3 settings in `on_install` (Setting.set pattern - see how other
|
|
plugins seed in `on_install`; category `printedparts`).
|
|
|
|
Checkpoint
|
|
```
|
|
pytest plugins/printedparts/tests/ -q # write tests as you go:
|
|
# - create mints 3DP-0001 style codes
|
|
# - restock/adjust move both ledger and cache atomically
|
|
# - adjust below zero -> 400/422
|
|
# - permission gates: anonymous create -> 401, wrong-perm user -> 403
|
|
curl -s localhost:5001/api/printedparts/items | jq # anonymous list OK
|
|
```
|
|
|
|
## Milestone 3 - management frontend + images
|
|
|
|
Build
|
|
1. The scaffold already dropped `PrintedpartsList/Detail/Form.vue` starters
|
|
and a router file; rename/build them into `PrintedItemsList/Detail/Form`
|
|
per the spec. Master templates: `PrintersList.vue` (list),
|
|
`PrinterDetail.vue` (detail). Global CSS only; CSS variables for colors
|
|
(frontend/CLAUDE.md rules).
|
|
2. Register the API client in `frontend/src/api/index.js` (paste the
|
|
generated `frontend-api-snippet.js`, extend with restock/adjust/image
|
|
calls).
|
|
3. Low-stock highlighting on the list (`quantityonhand <= lowstockthreshold`
|
|
-> danger badge). Filters: search + low-stock-only checkbox.
|
|
4. Image upload: replicate the models-image trio - upload/serve/delete -
|
|
from `shopdb/core/api/models.py` INTO the plugin blueprint
|
|
(`instance/printedpartsimages/`, public GET serve, imageurl column,
|
|
prefix-guarded delete). Wire the Form upload + Detail hero image.
|
|
5. Nav: `get_navigation_items()` -> "3D Parts" (usb plugin shape). Router
|
|
meta: list/detail plugin-gated, new/edit `requiresAuth` (see
|
|
`frontend/src/router/routes/usb.js`).
|
|
|
|
Checkpoint: create an item with a photo in the UI; thumbnail on list, hero on
|
|
detail; restock from detail updates qty + shows in history; frontend build +
|
|
`npx vitest run` green; naming hook green.
|
|
|
|
## Milestone 4 - badge resolution + kiosk
|
|
|
|
Build
|
|
1. Badge resolver in the plugin (`services/badges.py`): copy the USB contract
|
|
- all-digits -> SSO; `^0(\d+)BZ$` case-insensitive -> PayNo; resolve
|
|
display name via the employees plugin directory the way
|
|
`plugins/usb/api/selfhosted.py::_resolve_name` does (lazy import inside
|
|
the function, degrade gracefully when the plugin is absent). Policy
|
|
setting `printedparts_unknown_badge` (deny -> 422).
|
|
2. Kiosk endpoints (UNauthenticated - the notifications/employees open-read
|
|
precedent): `GET /kiosk/item/<itemcode>` and `POST /kiosk/take`
|
|
{itemcode, badge, quantity}. Take: validate active item, 1 <= qty <=
|
|
onhand, resolve badge, single-commit ledger row + decrement. Clear error
|
|
strings - the kiosk displays them verbatim.
|
|
3. Kiosk view `/parts-kiosk`: top-level route, NO requiresAuth, outside
|
|
AppLayout (register beside `/shopfloor` in `frontend/src/router/index.js`).
|
|
Three-step flow per the spec. The scanner is a keyboard wedge: hidden
|
|
always-focused input, submit on Enter, route the scan to whichever step is
|
|
active. Build `TouchKeypad.vue` (3x4 grid of big buttons, emits digits/
|
|
clear/backspace) - net-new, nothing to copy, keep it dumb.
|
|
4. Manual fallback path (typed item search + badge entry) behind a small
|
|
"no scanner?" link.
|
|
|
|
Checkpoint: full kiosk walkthrough on a touchscreen (or browser): scan/type
|
|
an itemcode -> item card; badge `0123456BZ` and plain SSO both resolve; take 3
|
|
-> success screen, qty down 3, ledger row has your name; taking more than
|
|
onhand -> friendly error; unknown badge -> denied message. Backend tests for
|
|
the resolver shapes + take validation.
|
|
|
|
## Milestone 5 - labels (1in x 0.5in)
|
|
|
|
Build
|
|
1. Public print route `/print/printedparts-labels` + view (imitate
|
|
`USBLabelBatch.vue` - USB is the precedent for a plugin OWNING its label
|
|
page instead of joining TYPE_CONFIG).
|
|
2. New stock size: `@page { size: 1in 0.5in; margin: 0 }`, one label per page
|
|
(roll-fed label printers treat each page as one label). Layout: CODE128
|
|
via JsBarcode (~0.9in wide, displayValue false), itemcode text ~7pt under
|
|
it, optional truncated name. Offer QR as a variant but default barcode.
|
|
3. Batch: multi-select items -> sequence of labels; plus a ULINE mini-grid
|
|
sheet fallback (mini72 pattern in `AssetLabelBatch.vue`).
|
|
4. Print buttons on Detail (single) and List (batch selected).
|
|
|
|
Checkpoint: print preview shows one 1x0.5 label per page; a printed (or
|
|
PDF-zoomed) barcode scans back into the kiosk and pulls up the right item.
|
|
That round trip - label printed from the catalog, scanned at the kiosk,
|
|
stock decremented with your name on it - is the demo moment; make it work
|
|
end to end before polishing.
|
|
|
|
## Milestone 6 - metrics, reports, widget
|
|
|
|
Build
|
|
1. `get_reports()` -> stock, consumption (date range), by-person; endpoints
|
|
in the plugin blueprint, `@jwt_required(optional=True)`, `?format=csv` via
|
|
the `generate_csv` helper pattern (`shopdb/core/api/reports.py` shows the
|
|
shape; a plugin report lives in the plugin and is merged into
|
|
`GET /api/reports` automatically when enabled).
|
|
2. Stock report includes the reconcile check: flag rows where cached
|
|
`quantityonhand` != SUM(ledger). Should always be empty; if not, you have
|
|
a non-atomic write path - find it.
|
|
3. `get_dashboard_widgets()` -> low-stock count.
|
|
4. Nice-to-have if time: burn rate (avg weekly takes over trailing 4 weeks +
|
|
weeks-to-empty). Plain SQL over the ledger.
|
|
|
|
Checkpoint: reports appear on /reports grouped under the plugin, CSV
|
|
downloads; widget renders on the dashboard; reconcile column all-clear after
|
|
a kiosk session.
|
|
|
|
## Milestone 7 - lifecycle + closeout
|
|
|
|
Build/verify
|
|
1. Disable/enable cycle: `flask plugin disable printedparts` - nav entry,
|
|
routes, reports, and grantable permissions all disappear; enable restores.
|
|
2. Fresh-database proof: point DATABASE_URL at a scratch DB, `flask db
|
|
upgrade` + `flask plugin install/enable/upgrade-all` - everything works
|
|
with zero manual SQL.
|
|
3. Full suite: backend pytest, vitest, frontend build, naming hook.
|
|
4. End checklist from `PLUGIN-GUIDE.md` section 12.
|
|
|
|
Done means: a colleague can clone the repo, enable the plugin, print a bin
|
|
label, and take a part at the kiosk with their badge - without asking you
|
|
anything.
|
|
|
|
---
|
|
|
|
## Where each pattern lives (cheat sheet)
|
|
|
|
| Need | Copy from |
|
|
|---|---|
|
|
| Standalone (non-asset) plugin shape | `plugins/knowledgebase/` |
|
|
| Checkout/ledger + badge contract | `plugins/usb/` (`api/routes.py` badge regex, `api/selfhosted.py` name resolve) |
|
|
| Real-baseline plugin migration | `plugins/measuringtools/migrations/` |
|
|
| Blueprint style, pagination, authz | `plugins/measuringtools/api/routes.py` |
|
|
| Image upload/serve/delete | `shopdb/core/api/models.py` |
|
|
| Open kiosk endpoints precedent | `plugins/employees/api/routes.py`, `plugins/notifications/api/routes.py` |
|
|
| Plugin-owned label print view | `frontend/src/views/print/USBLabelBatch.vue` |
|
|
| Barcode/QR rendering | JsBarcode usage in `AssetLabel.vue`, `qrLogo.js` |
|
|
| Kiosk route posture | `/shopfloor` in `frontend/src/router/index.js` |
|
|
| List/Detail master templates | `PrintersList.vue`, `PrinterDetail.vue` |
|
|
| Reports hook + CSV | `plugins/warranty/` report + `shopdb/core/api/reports.py` |
|
|
| Permissions declaration | `plugins/usb/plugin.py::get_permissions` |
|