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 @@
+
+
+
- | SKU |
- DESCRIPTION |
- QTY |
- UOM |
- UNIT PRICE |
+ SKU |
+ DESCRIPTION |
+ QTY |
+ UOM |
+ UNIT PRICE |
+ DISCOUNT |
TAX |
AMOUNT |
@@ -93,10 +97,10 @@
- |
+ |
- |
+ |
@@ -109,6 +113,10 @@
|
|
+
+ %
+ -
+ |
|
@@ -139,9 +147,23 @@
| Subtotal |
-
+
|
+
+
+ | Discount |
+
+
+ |
+
+
+ | Net Amount |
+
+
+ |
+
+
| Taxes |
@@ -254,25 +276,29 @@
|
+
+
+
| SKU |
- DESCRIPTION |
+ DESCRIPTION |
QTY |
UOM |
UNIT PRICE |
+ DISCOUNT |
TAX |
- AMOUNT |
+ AMOUNT |
- |
+ |
- |
+ |
@@ -285,6 +311,10 @@
|
|
+
+ %
+ -
+ |
|
@@ -315,9 +345,23 @@
| 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 @@
|
+
+
+
- | SKU |
- DESCRIPTION |
- QTY |
- UOM |
- UNIT PRICE |
+ SKU |
+ DESCRIPTION |
+ QTY |
+ UOM |
+ UNIT PRICE |
+ DISCOUNT |
TAX |
AMOUNT |
@@ -79,10 +83,10 @@
- |
+ |
- |
+ |
@@ -101,6 +105,10 @@
|
|
+
+ %
+ -
+ |
|
@@ -125,9 +133,23 @@
| Subtotal |
-
+
|
+
+
+ | Discount |
+
+
+ |
+
+
+ | Net Amount |
+
+
+ |
+
+
| Taxes |
@@ -236,6 +258,7 @@
+
@@ -302,9 +325,23 @@
| Subtotal |
-
+
|
+
+
+ | Discount |
+
+
+ |
+
+
+ | Net Amount |
+
+
+ |
+
+
| Taxes |
| |