diff --git a/fusion_login_audit/models/res_users.py b/fusion_login_audit/models/res_users.py index 818fffb2..788bf20c 100644 --- a/fusion_login_audit/models/res_users.py +++ b/fusion_login_audit/models/res_users.py @@ -346,15 +346,30 @@ class ResUsers(models.Model): string='Login Audit Count', compute='_compute_x_fc_login_audit_count', ) + # NON-STORED on purpose — do NOT re-add store=True. + # + # These were store=True computed-from-the-audit-One2many. That meant every + # successful-login audit row (written through an INDEPENDENT + # registry.cursor(), see _fc_record_login_event) forced a recompute that + # flushed a write-back onto THIS res_users row. During portal-invitation + # acceptance the request has already locked that row (auth_signup just set + # the password in the same transaction), so the audit cursor's write-back + # blocked on the request's own row lock while the request's Python blocked + # waiting for the audit cursor — a self-deadlock Postgres cannot detect + # (the holder shows 'idle in transaction', not lock-waiting). Workers + # wedged for up to limit_time_real (20 min) and odoo-westin went + # unresponsive every time an invite was accepted (issue 2026-06-03). + # + # Keeping them non-stored means creating an audit row never touches + # res_users. They compute on read (display-only on the user form). The + # regression guard is tests.test_last_login_fields_not_stored. 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') diff --git a/fusion_login_audit/tests/test_login_audit.py b/fusion_login_audit/tests/test_login_audit.py index 4cf74c8f..388e193a 100644 --- a/fusion_login_audit/tests/test_login_audit.py +++ b/fusion_login_audit/tests/test_login_audit.py @@ -303,6 +303,54 @@ class TestFusionLoginAuditModel(TransactionCase): self.assertGreaterEqual(user.x_fc_login_audit_count, 1) self.assertEqual(user.x_fc_last_login_ip, '198.51.100.42') + def test_last_login_fields_not_stored(self): + """Regression guard for the 2026-06-03 invitation-acceptance hang. + + x_fc_last_successful_login / x_fc_last_login_ip MUST stay non-stored. + When they were store=True (computed from the audit One2many), creating + the success audit row through the independent registry cursor forced a + write-back onto the very res_users row the request had already locked + (auth_signup had just set the password) -> a self-deadlock Postgres + cannot see (the holder shows 'idle in transaction'). Workers wedged for + up to limit_time_real and odoo-westin became unresponsive whenever an + invitation was accepted. Non-stored means audit-row creation never + touches res_users, so the deadlock cannot form. + """ + fields_ = self.env['res.users']._fields + self.assertFalse( + fields_['x_fc_last_successful_login'].store, + "x_fc_last_successful_login must be non-stored (see docstring)") + self.assertFalse( + fields_['x_fc_last_login_ip'].store, + "x_fc_last_login_ip must be non-stored (see docstring)") + + def test_audit_row_create_does_not_write_res_users(self): + """Creating a login-audit row must not write the linked res_users row. + + This is the behavioural half of the deadlock guard: with the fields + non-stored, inserting an audit row for a user leaves that user's + write_date untouched (no recompute -> no res_users UPDATE -> nothing + to contend with the request's own row lock). + """ + user = self.env['res.users'].sudo().create({ + 'name': 'NoWriteback Tester', + 'login': 'nowriteback-tester@example.com', + 'password': 'nowriteback-tester-pw-1', + }) + user.flush_recordset() + before = user.write_date + self.env['fusion.login.audit'].sudo().create({ + 'user_id': user.id, + 'attempted_login': user.login, + 'result': 'success', + 'database': self.env.cr.dbname, + 'ip_address': '198.51.100.7', + }) + user.invalidate_recordset() + self.assertEqual( + user.write_date, before, + "Audit-row create must not write back to res_users") + def test_action_view_login_audit_returns_window_action(self): """The smart-button action returns an act_window scoped to this user.""" user = self.env['res.users'].sudo().create({ diff --git a/fusion_reports_templates/__manifest__.py b/fusion_reports_templates/__manifest__.py index 12d68440..1b20b2d8 100644 --- a/fusion_reports_templates/__manifest__.py +++ b/fusion_reports_templates/__manifest__.py @@ -3,7 +3,7 @@ # License OPL-1 (Odoo Proprietary License v1.0) { 'name': 'Fusion Reports — Templates', - 'version': '19.0.1.2.0', + 'version': '19.0.1.3.0', 'category': 'Tools/Reports', 'summary': 'Branded PDF templates for Quotation, Sales Order, Invoice, Delivery, Purchase Order, and Payment Receipt.', 'description': """ diff --git a/fusion_reports_templates/report/report_invoice.xml b/fusion_reports_templates/report/report_invoice.xml index 244747fd..11eb37d4 100644 --- a/fusion_reports_templates/report/report_invoice.xml +++ b/fusion_reports_templates/report/report_invoice.xml @@ -78,14 +78,18 @@ + + + - - - - - + + + + + + @@ -93,10 +97,10 @@ - + - + @@ -109,6 +113,10 @@ + @@ -139,9 +147,23 @@ + + + + + + + + + +
SKUDESCRIPTIONQTYUOMUNIT PRICESKUDESCRIPTIONQTYUOMUNIT PRICEDISCOUNT TAX AMOUNT
+ % + - +
Subtotal - +
Discount + +
Net Amount + +
Taxes @@ -254,25 +276,29 @@
+ + + - + + - + - + - + @@ -285,6 +311,10 @@ + @@ -315,9 +345,23 @@ + + + + + + + + + +
SKUDESCRIPTIONDESCRIPTION QTY UOM UNIT PRICEDISCOUNT TAXAMOUNTAMOUNT
+ % + - +
Subtotal - +
Discount + +
Net Amount + +
Taxes diff --git a/fusion_reports_templates/report/report_sale.xml b/fusion_reports_templates/report/report_sale.xml index 8f008d8e..5d7a908a 100644 --- a/fusion_reports_templates/report/report_sale.xml +++ b/fusion_reports_templates/report/report_sale.xml @@ -64,14 +64,18 @@
+ + + - - - - - + + + + + + @@ -79,10 +83,10 @@ - + - + @@ -101,6 +105,10 @@ + @@ -125,9 +133,23 @@ + + + + + + + + + +
SKUDESCRIPTIONQTYUOMUNIT PRICESKUDESCRIPTIONQTYUOMUNIT PRICEDISCOUNT TAX AMOUNT
+ % + - +
Subtotal - +
Discount + +
Net Amount + +
Taxes @@ -236,6 +258,7 @@ + @@ -302,9 +325,23 @@ + + + + + + + + + +
Subtotal - +
Discount + +
Net Amount + +
Taxes