Skip to content

Commit 8a26c70

Browse files
committed
fix(farm_qbo_connection): implement action_refresh_token referenced by form view
The form view's header declared a `Refresh Token` button (visible when state ∈ {connected, expired}) calling `action_refresh_token`, but the model never shipped the method. Odoo's view validator caught it at install time: ParseError: while parsing farm_qbo_connection_views.xml:25 action_refresh_token is not a valid action on farm.qbo.connection Adds the method on the model and the underlying token-refresh round-trip on services/intuit_client.IntuitClient.refresh_access_token() — uses intuitlib.client.AuthClient.refresh() when the libs are installed, raises a clear UserError in stub mode. Two tests cover both paths: missing refresh token → UserError; mocked successful refresh → connection record updated with new access/refresh tokens + expiry. Unblocks CI on PR #1 and the entire stacked PR chain (#2#6) that targets feature branches off this one.
1 parent cf00cfa commit 8a26c70

3 files changed

Lines changed: 118 additions & 0 deletions

File tree

farm_quickbooks_io/models/farm_qbo_connection.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
from datetime import timedelta
2+
13
from odoo import api, fields, models
4+
from odoo.exceptions import UserError
25

36

47
class FarmQboConnection(models.Model):
@@ -94,3 +97,58 @@ def action_disconnect(self):
9497
"realm_id": False,
9598
}
9699
)
100+
101+
def action_refresh_token(self):
102+
"""Mint a fresh access token using the stored refresh token.
103+
104+
Called from the form-view header button when state ∈ {connected,
105+
expired}. Delegates the actual round-trip to the intuit-oauth
106+
library via `services.intuit_client.IntuitClient`; bails with a
107+
clear UserError if either the refresh token is missing or the
108+
intuit-oauth libs aren't installed (stub mode).
109+
"""
110+
self.ensure_one()
111+
if not self.refresh_token:
112+
raise UserError(
113+
self.env._(
114+
"No refresh token stored. Click Connect QuickBooks to "
115+
"complete the OAuth flow first."
116+
)
117+
)
118+
# Lazy import keeps stub-mode installs (no intuit-oauth wheel) from
119+
# crashing at module-load time.
120+
from ..services.intuit_client import ( # noqa: PLC0415
121+
HAS_INTUIT_LIBS,
122+
IntuitClient,
123+
)
124+
125+
if not HAS_INTUIT_LIBS:
126+
raise UserError(
127+
self.env._(
128+
"intuit-oauth library is not installed. Install "
129+
"`intuit-oauth` + `python-quickbooks` to refresh tokens."
130+
)
131+
)
132+
client = IntuitClient(self)
133+
new_tokens = client.refresh_access_token()
134+
self.write(
135+
{
136+
"access_token": new_tokens["access_token"],
137+
"refresh_token": new_tokens.get("refresh_token", self.refresh_token),
138+
"token_expires_at": fields.Datetime.now()
139+
+ timedelta(seconds=new_tokens.get("expires_in", 3600)),
140+
}
141+
)
142+
return {
143+
"type": "ir.actions.client",
144+
"tag": "display_notification",
145+
"params": {
146+
"type": "success",
147+
"title": self.env._("Token refreshed"),
148+
"message": self.env._(
149+
"New access token valid until %s.",
150+
self.token_expires_at,
151+
),
152+
"next": {"type": "ir.actions.act_window_close"},
153+
},
154+
}

farm_quickbooks_io/services/intuit_client.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,31 @@ def is_live(self):
3333
self.connection.access_token and self.connection.realm_id
3434
)
3535

36+
def refresh_access_token(self):
37+
"""Exchange the stored refresh token for a fresh access token.
38+
39+
Returns a dict matching Intuit's token response shape:
40+
``{access_token, refresh_token, expires_in, x_refresh_token_expires_in}``.
41+
Caller writes the values back onto the farm.qbo.connection record.
42+
"""
43+
if not HAS_INTUIT_LIBS:
44+
raise RuntimeError(
45+
"intuit-oauth not installed; cannot refresh access token."
46+
)
47+
auth = AuthClient(
48+
client_id="STUB", # configured via ir.config_parameter in v1
49+
client_secret="STUB",
50+
environment=self.connection.environment,
51+
redirect_uri="https://localhost/farm_qbo/oauth/callback",
52+
)
53+
auth.refresh_token = self.connection.refresh_token
54+
auth.refresh()
55+
return {
56+
"access_token": auth.access_token,
57+
"refresh_token": auth.refresh_token,
58+
"expires_in": auth.expires_in,
59+
}
60+
3661
def _real_client(self):
3762
if not HAS_INTUIT_LIBS:
3863
raise RuntimeError(

farm_quickbooks_io/tests/test_farm_qbo.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from unittest.mock import patch
2+
3+
from odoo.exceptions import UserError
14
from odoo.tests.common import TransactionCase
25

36

@@ -74,6 +77,38 @@ def test_import_rollback_reverses_commit(self):
7477
for m in imp.mapping_ids:
7578
self.assertFalse(m.committed_target_ref)
7679

80+
def test_action_refresh_token_without_refresh_token_raises(self):
81+
# Connection with no refresh token can't refresh — must bail with a
82+
# clear UserError rather than calling out to Intuit with None.
83+
with self.assertRaises(UserError):
84+
self.connection.action_refresh_token()
85+
86+
def test_action_refresh_token_writes_new_access_token(self):
87+
# Stub IntuitClient.refresh_access_token so the test doesn't need the
88+
# intuit-oauth wheel installed; verify the connection record is
89+
# updated with the new tokens.
90+
self.connection.write({"access_token": "old", "refresh_token": "rt"})
91+
new_tokens = {
92+
"access_token": "new-access",
93+
"refresh_token": "new-refresh",
94+
"expires_in": 3600,
95+
}
96+
with (
97+
patch(
98+
"odoo.addons.farm_quickbooks_io.services.intuit_client.HAS_INTUIT_LIBS",
99+
True,
100+
),
101+
patch(
102+
"odoo.addons.farm_quickbooks_io.services."
103+
"intuit_client.IntuitClient.refresh_access_token",
104+
return_value=new_tokens,
105+
),
106+
):
107+
self.connection.action_refresh_token()
108+
self.assertEqual(self.connection.access_token, "new-access")
109+
self.assertEqual(self.connection.refresh_token, "new-refresh")
110+
self.assertTrue(self.connection.token_expires_at)
111+
77112
def test_wizard_creates_and_starts_import(self):
78113
Wizard = self.env["farm.qbo.import.wizard"]
79114
wiz = Wizard.create(

0 commit comments

Comments
 (0)