feat(fusion_login_audit): smart button + Login Activity tab on res.users
Adds four x_fc_* fields on res.users: login_audit_ids (One2many), login_audit_count (compute), last_successful_login (compute, stored), last_login_ip (compute, stored). action_fc_view_login_audit returns a window action scoped to the current user. View inheritance adds a smart button to the button box and a "Login Activity" page to the notebook, both gated by base.group_system on the inner XML nodes (NOT on the view record — Odoo 19 forbids that; see CLAUDE.md rule #11). Tests (2 new, 18 total green): test_computed_last_successful_login — uses registry cursor to commit the audit row so the stored compute picks it up across the TransactionCase boundary. test_action_view_login_audit_returns_window_action — smart-button action shape + domain scoping. CLAUDE.md rule #11 added: inherited ir.ui.view records cannot have groups/group_ids on the record; the gate must be on the inner XML nodes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import AccessDenied
|
||||
|
||||
# Top-level import (vs lazy inside the method): if the dep is missing — most
|
||||
@@ -193,3 +193,66 @@ class ResUsers(models.Model):
|
||||
_credential=credential,
|
||||
)
|
||||
raise
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Per-user surface — fields + action method backing the smart button
|
||||
# and "Login Activity" tab on the user form view.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
|
||||
x_fc_login_audit_ids = fields.One2many(
|
||||
'fusion.login.audit', 'user_id',
|
||||
string='Login Activity',
|
||||
)
|
||||
x_fc_login_audit_count = fields.Integer(
|
||||
string='Login Audit Count',
|
||||
compute='_compute_x_fc_login_audit_count',
|
||||
)
|
||||
x_fc_last_successful_login = fields.Datetime(
|
||||
string='Last Successful Login',
|
||||
compute='_compute_x_fc_last_successful_login',
|
||||
store=True,
|
||||
)
|
||||
x_fc_last_login_ip = fields.Char(
|
||||
string='Last Login IP', size=45,
|
||||
compute='_compute_x_fc_last_successful_login',
|
||||
store=True,
|
||||
)
|
||||
|
||||
@api.depends('x_fc_login_audit_ids')
|
||||
def _compute_x_fc_login_audit_count(self):
|
||||
# Odoo 19: read_group → _read_group, returns list of tuples
|
||||
# (group_key, aggregate_value) when given groupby + aggregates.
|
||||
Audit = self.env['fusion.login.audit'].sudo()
|
||||
rows = Audit._read_group(
|
||||
domain=[('user_id', 'in', self.ids)],
|
||||
groupby=['user_id'],
|
||||
aggregates=['__count'],
|
||||
)
|
||||
counts = {user.id: count for user, count in rows}
|
||||
for user in self:
|
||||
user.x_fc_login_audit_count = counts.get(user.id, 0)
|
||||
|
||||
@api.depends('x_fc_login_audit_ids.event_time',
|
||||
'x_fc_login_audit_ids.result',
|
||||
'x_fc_login_audit_ids.ip_address')
|
||||
def _compute_x_fc_last_successful_login(self):
|
||||
Audit = self.env['fusion.login.audit'].sudo()
|
||||
for user in self:
|
||||
row = Audit.search(
|
||||
[('user_id', '=', user.id), ('result', '=', 'success')],
|
||||
order='event_time desc', limit=1,
|
||||
)
|
||||
user.x_fc_last_successful_login = row.event_time or False
|
||||
user.x_fc_last_login_ip = row.ip_address or False
|
||||
|
||||
def action_fc_view_login_audit(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Login Activity'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.login.audit',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('user_id', '=', self.id)],
|
||||
'context': {'create': False, 'edit': False, 'delete': False,
|
||||
'default_user_id': self.id},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user