5-min cron processes up to 100 pending rows per pass: private IPs
short-circuit to state=private_ip; same-IP cache (30 days) avoids
duplicate ip-api.com calls; reverse DNS via socket with 1.5s timeout;
HTTP lookup respects ip-api''s X-Rl rate-limit header. Tests cover
private-IP shortcut, cache hit (no HTTP), and internal-state skip --
no network calls needed.
Per-row isolation uses cr.savepoint() instead of cr.commit() because
Odoo 19 TestCursor raises AssertionError on commit/rollback. Recorded
the gotcha as CLAUDE.md rule #14.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds _fc_retention_gc() that deletes rows older than the configured
horizon (default 365 days; 0 = keep forever). Registered as a daily
ir.cron. Tests verify both the delete path and the "keep forever"
short-circuit.
Also documents the Odoo 19 gotcha that ir.cron dropped the numbercall
field (the legacy "-1 = run forever" pattern now raises ValueError at
install time; just omit the field).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- All 16 columns per spec (user, attempted_login, result, failure_reason,
event_time, ip/geo fields, user_agent triple, device_type, database).
- Check constraint binds failure_reason presence to result value.
- Three composite indexes (user+time, login+time, geo_state+time) supporting
the per-user, failure-burst, and geo cron queries.
- Minimal admin-read ACL added so subsequent tests can verify writes.
- 3 TransactionCase tests passing: model create, failure_reason nullable on
success, geo_lookup_state='internal' accepted.
Odoo 19 deprecation note: this implementation uses the declarative
models.Constraint and models.Index attributes (Odoo 19 silently drops the
legacy `_sql_constraints = [...]` list and `init()`/raw-SQL pattern with
only a warning). Captured in CLAUDE.md rule #9.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>