feat(fusion_login_audit): settings model + page section

Four x_fc_* fields on res.config.settings backed by ir.config_parameter:
retention_days (default 365, 0 = forever), alert_threshold (5),
alert_window_min (15), alert_enabled (True). New "Login Audit" block
on the General Settings page (gated by base.group_system on the block,
NOT on the inherited view record per CLAUDE.md rule #11).

CLAUDE.md gotchas added during this task:
  #5 Boolean config_parameter fields don't round-trip "False" as a
     string — IrConfigParameter.set_param deletes the row on falsy.
     Test with assertFalse, never assertEqual(..., "False").
  #6 ir.ui.view uses group_ids (Odoo 19 rename mirrored from res.users).
     Setting groups_id on an ir.ui.view record raises ValueError at
     install. (The XML attribute groups="..." on inner nodes is
     unrelated and still works.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-26 21:27:13 -04:00
parent 0513ea23a4
commit 6f6aa6e90a
6 changed files with 89 additions and 1 deletions

View File

@@ -12,6 +12,7 @@
3. **Backend OWL**: Use standalone `rpc()` from `@web/core/network/rpc`. NOT `useService("rpc")`. `static props = []` not `{}`.
4. **HTTP routes**: `type="jsonrpc"` — NOT `type="json"` (deprecated).
5. **res.config.settings**: Only boolean/integer/float/char/selection/many2one/datetime. NO Date fields.
**`config_parameter=` Boolean fields don't round-trip `False` as a string.** Odoo's `set_values()` calls `IrConfigParameter.set_param(key, value)`, and `set_param` deletes the row when `value` is falsy (False / None / empty). So writing `False` to a Boolean config field means the param no longer exists in `ir_config_parameter`; a subsequent `get_param(key)` returns the *default* (Python `False`), not `'False'`. Test like `self.assertFalse(ICP.get_param('...'))` — never `assertEqual(..., 'False')`. (Integer/Float/Char go through `repr(value)` / strip, so they DO persist as strings — `'90'`, `'0'`, etc.) Source: `odoo/addons/base/models/res_config.py::set_values` and `ir_config_parameter.py::set_param`.
6. **res.groups**: NO `users` field, NO `category_id` field.
**res.users**: field was renamed `groups_id` → `group_ids` (also `all_group_ids` for implied). The plural form is gone; using `groups_id` raises `ValueError: Invalid field 'groups_id' in 'res.users'`.
**`ir.ui.view`**: same rename — view-level visibility gating uses `group_ids`, not `groups_id`. A record like `<field name="groups_id" eval="[(4, ref('base.group_system'))]"/>` on an `ir.ui.view` raises `ValueError: Invalid field 'groups_id' in 'ir.ui.view'` at module install. (The XML *attribute* `groups="base.group_system"` on form elements like `<page>`, `<button>`, `<field>` is unrelated and still works.)

View File

@@ -19,7 +19,7 @@ bursts. Daily retention cron honours a configurable horizon.
'author': 'Nexa Systems Inc.',
'website': 'https://nexasystems.ca',
'license': 'OPL-1',
'depends': ['base', 'mail'],
'depends': ['base', 'mail', 'base_setup'],
'external_dependencies': {
'python': ['user_agents'],
},
@@ -28,6 +28,7 @@ bursts. Daily retention cron honours a configurable horizon.
'security/security.xml',
'views/fusion_login_audit_views.xml',
'views/res_users_views.xml',
'views/res_config_settings_views.xml',
'views/menus.xml',
],
'installable': True,

View File

@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import fusion_login_audit
from . import res_users
from . import res_config_settings

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
x_fc_login_audit_retention_days = fields.Integer(
string='Login Audit Retention (days)',
default=365,
config_parameter='fusion_login_audit.retention_days',
help='Login audit rows older than this are deleted by the nightly '
'cron. Set to 0 to keep forever.',
)
x_fc_login_audit_alert_threshold = fields.Integer(
string='Alert After N Consecutive Failures',
default=5,
config_parameter='fusion_login_audit.alert_threshold',
help='When this many failures for the same attempted login occur '
'within the alert window, Settings admins receive one email.',
)
x_fc_login_audit_alert_window_min = fields.Integer(
string='Alert Window (minutes)',
default=15,
config_parameter='fusion_login_audit.alert_window_min',
)
x_fc_login_audit_alert_enabled = fields.Boolean(
string='Send Failed-Login Alerts',
default=True,
config_parameter='fusion_login_audit.alert_enabled',
)

View File

@@ -266,3 +266,21 @@ class TestFusionLoginAuditModel(TransactionCase):
self.assertEqual(action['type'], 'ir.actions.act_window')
# Domain must filter to this user
self.assertIn(('user_id', '=', user.id), action['domain'])
def test_settings_round_trip(self):
"""Writing settings persists them via ir.config_parameter."""
Settings = self.env['res.config.settings'].sudo()
Settings.create({
'x_fc_login_audit_retention_days': 90,
'x_fc_login_audit_alert_threshold': 3,
'x_fc_login_audit_alert_window_min': 5,
'x_fc_login_audit_alert_enabled': False,
}).execute()
ICP = self.env['ir.config_parameter'].sudo()
self.assertEqual(ICP.get_param('fusion_login_audit.retention_days'), '90')
self.assertEqual(ICP.get_param('fusion_login_audit.alert_threshold'), '3')
self.assertEqual(ICP.get_param('fusion_login_audit.alert_window_min'), '5')
# Odoo's set_param deletes the row when the value is falsy, so a
# Boolean field set to False yields get_param() == False (Python
# bool, the default), not the string 'False'.
self.assertFalse(ICP.get_param('fusion_login_audit.alert_enabled'))

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_res_config_settings_form_login_audit" model="ir.ui.view">
<field name="name">res.config.settings.form.login.audit</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//block[@id='user_default_rights']" position="after">
<block title="Login Audit"
name="login_audit_block"
groups="base.group_system">
<setting id="login_audit_retention"
string="Retention (days)"
help="0 = keep forever">
<field name="x_fc_login_audit_retention_days"/>
</setting>
<setting id="login_audit_alert_enabled"
string="Send failed-login alerts"
help="Email Settings admins when consecutive failures cross the threshold">
<field name="x_fc_login_audit_alert_enabled"/>
</setting>
<setting id="login_audit_alert_threshold"
string="Alert threshold (failures)">
<field name="x_fc_login_audit_alert_threshold"/>
</setting>
<setting id="login_audit_alert_window"
string="Alert window (minutes)">
<field name="x_fc_login_audit_alert_window_min"/>
</setting>
</block>
</xpath>
</field>
</record>
</odoo>