-
Notifications
You must be signed in to change notification settings - Fork 1
Developer guide
nexus_zkt_integration/
├── hooks.py app metadata, the two scheduled jobs, /apps tile
├── api.py has_app_permission for the /apps tile
├── install.py uninstall.py
└── nexus_biometric_attendance/
├── punch.py the rules. imports nothing from Frappe
├── reader.py everything that speaks the ZK protocol
├── journal.py the activity log, and the live progress bar
├── collector.py one run: read every machine, then write
├── api.py the four whitelisted methods, and the hourly job
├── doctype/
│ ├── nexus_zkt_settings/ the Single that holds the device list
│ ├── nexus_zkt_device/ one row in it
│ └── nexus_attendance_log/ what a run wrote down
└── workspace/nexus_zkt/
The split is deliberate. punch.py holds the only part worth reasoning about
carefully — whether a punch is an arrival or a departure — and it has no database,
no socket and no Frappe import, so it can be tested on its own and read in one
sitting.
api.collect_now(force=1) the button, and bench execute
api.scheduled_collection() the hourly job; steps aside if one is running
│
└── Collector.collect()
├── _read_every_device() one Take per machine that answered
└── _record() every punch, merged, oldest first
_read_every_device returns a Take per machine — the device row, when it was
read, its punches, and a Tally. _record then merges every take's punches into
one time-ordered stream before deciding anything, which is what makes more than
one door work. Each punch is still judged by its own machine's DirectionRule.
| Method | Whitelisted | Used by |
|---|---|---|
api.collect_now(force=0) |
yes | Sync Attendance Now, bench execute
|
api.sync_status() |
yes | the settings form, polling |
api.purge_journal() |
yes | Clear Logs, and the weekly job |
api.erase_device(device_id) |
yes | Clear Device Memory |
api.scheduled_collection() |
no |
hourly_long in hooks.py
|
version-16 is the default branch; version-15 sits beside it, not behind it.
Neither is ahead of the other, so GitHub does not offer a pull request between
them. Only four files differ, and none of them is Python: the README banner,
the CI matrix, a comment in pyproject.toml, and the workspace JSON — Frappe 15's
Workspace DocType has no app field.
Two Frappe differences are worth knowing if you touch this code:
-
frappe.cacheis a module attribute on both, not a function. -
Frappe 15 caches a cache miss.
get_value(key)stores what it found infrappe.local.cacheeven when that is nothing, whileset_value(key, val, expires_in_sec=...)writes only to Redis and never refreshes that local copy. Read the key once before the first write andNoneis pinned for the rest of the request. Every read of the run status therefore goes throughjournal.read_status(), which passesexpires=True. This is invisible on Frappe 16, which is exactly why both branches are tested.
bench --site your-site set-config allow_tests true
bench --site your-site run-tests --app nexus_zkt_integration106 tests. Twenty-seven of them are plain unittest and need no site, because
punch.py needs none.
| File | Covers |
|---|---|
test_punch_rules.py |
arrival/departure rules, the repeat window, the open-shift limit |
test_many_doors.py |
merging across machines, fixed-direction doors, ports |
test_collector.py |
shift mapping, the wait between reads, the journal, run status |
test_settings.py |
duplicate names, duplicate machines, the password field |
test_app_wiring.py |
every dotted path in hooks.py, every method the client calls |
CI runs the whole suite against frappe + erpnext + hrms at both 15 and 16 on
every push, and asserts frappe.__version__ really starts with the number the
branch claims — a bench init without --frappe-branch quietly builds develop.
Replace the reader. It is one function:
from nexus_zkt_integration.nexus_biometric_attendance import collector
from nexus_zkt_integration.nexus_biometric_attendance.punch import Punch
def fake(device, on_warning=None):
return [Punch("101", datetime(2026, 4, 6, 9, 0), 255)]
collector.read_punches = fakeread_punches(device, on_warning=None) must return a list of Punch, and is
called once per device, so a dict keyed by device.device_id gives each machine
its own answer.
ruff for Python, prettier for JavaScript, both wired into .pre-commit-config.yaml:
pre-commit install
pre-commit run --all-filesTabs, double quotes, 110 columns.
Getting started
Using it
When it goes wrong
For developers
Legal