diff --git a/pms_smartlock_base/__manifest__.py b/pms_smartlock_base/__manifest__.py
index 95516d7d..5e2c33c6 100644
--- a/pms_smartlock_base/__manifest__.py
+++ b/pms_smartlock_base/__manifest__.py
@@ -18,6 +18,7 @@
"security/ir.model.access.csv",
"data/ir_cron.xml",
"wizards/lock_code_pin_viewer_views.xml",
+ "wizards/lock_list_wizard_views.xml",
"views/lock_vendor_views.xml",
"views/lock_code_views.xml",
"views/lock_code_access_log_views.xml",
diff --git a/pms_smartlock_base/models/lock_vendor.py b/pms_smartlock_base/models/lock_vendor.py
index 854c9da8..19e065a8 100644
--- a/pms_smartlock_base/models/lock_vendor.py
+++ b/pms_smartlock_base/models/lock_vendor.py
@@ -67,6 +67,34 @@ def get_connector(self):
_("No connector implementation for vendor '%s'.") % self.name
)
+ def action_fetch_locks(self):
+ """Fetch the vendor's locks and show them as plain text (name + device
+ id, one per line) so the operator can read the ids needed to configure
+ rooms. Read-only: it connects and lists, changing nothing."""
+ self.ensure_one()
+ connector = self.get_connector()
+ try:
+ locks = connector.list_locks()
+ except NotImplementedError as exc:
+ raise UserError(
+ _("Vendor '%s' does not support listing locks yet.") % self.name
+ ) from exc
+ lines = [f"{lock.get('name') or ''}\t{lock.get('id') or ''}" for lock in locks]
+ wizard = self.env["lock.list.wizard"].create(
+ {
+ "vendor_id": self.id,
+ "lock_listing": "\n".join(lines) or _("No locks returned."),
+ }
+ )
+ return {
+ "type": "ir.actions.act_window",
+ "name": _("Locks"),
+ "res_model": "lock.list.wizard",
+ "res_id": wizard.id,
+ "view_mode": "form",
+ "target": "new",
+ }
+
def _required_env(self, name):
"""Read a required environment variable holding a Roomdoo app
credential (vendor ``client_id``/``client_secret``). These identify
diff --git a/pms_smartlock_base/security/ir.model.access.csv b/pms_smartlock_base/security/ir.model.access.csv
index 089c7920..a0f94a53 100644
--- a/pms_smartlock_base/security/ir.model.access.csv
+++ b/pms_smartlock_base/security/ir.model.access.csv
@@ -5,6 +5,7 @@ access_lock_code_manager,lock.code.manager,model_lock_code,pms.group_pms_manager
access_lock_code_user,lock.code.user,model_lock_code,pms.group_pms_user,1,0,0,0
access_lock_code_access_log_admin,lock.code.access.log.admin,model_lock_code_access_log,pms_smartlock_base.group_smartlock_admin,1,0,0,0
access_lock_code_pin_viewer_user,lock.code.pin.viewer.user,model_lock_code_pin_viewer,pms.group_pms_user,1,1,1,1
+access_lock_list_wizard_admin,lock.list.wizard.admin,model_lock_list_wizard,pms_smartlock_base.group_smartlock_admin,1,1,1,1
access_lock_code_target_admin,lock.code.target.admin,model_lock_code_target,pms_smartlock_base.group_smartlock_admin,1,1,1,1
access_lock_code_target_manager,lock.code.target.manager,model_lock_code_target,pms.group_pms_manager,1,0,0,0
access_lock_code_target_user,lock.code.target.user,model_lock_code_target,pms.group_pms_user,1,0,0,0
diff --git a/pms_smartlock_base/tests/__init__.py b/pms_smartlock_base/tests/__init__.py
index 189f6c1d..5e611660 100644
--- a/pms_smartlock_base/tests/__init__.py
+++ b/pms_smartlock_base/tests/__init__.py
@@ -1,6 +1,7 @@
from . import test_cancel
from . import test_common_lock_targets
from . import test_cron
+from . import test_fetch_locks
from . import test_lock_code_state
from . import test_lock_code_sync
from . import test_pin_reveal
diff --git a/pms_smartlock_base/tests/test_fetch_locks.py b/pms_smartlock_base/tests/test_fetch_locks.py
new file mode 100644
index 00000000..8abdb894
--- /dev/null
+++ b/pms_smartlock_base/tests/test_fetch_locks.py
@@ -0,0 +1,52 @@
+from unittest.mock import MagicMock, patch
+
+from odoo.exceptions import UserError
+
+from .common import CommonSmartlock
+
+
+class TestFetchLocks(CommonSmartlock):
+ """Cover ``lock.vendor.action_fetch_locks``: it asks the connector for the
+ locks and renders them as plain text in a transient wizard, staying
+ vendor-agnostic and degrading gracefully when a vendor can't list."""
+
+ def setUp(self):
+ super().setUp()
+ self.connector = MagicMock(name="FakeConnector")
+ patcher = patch.object(
+ self.env.registry["lock.vendor"],
+ "get_connector",
+ return_value=self.connector,
+ )
+ patcher.start()
+ self.addCleanup(patcher.stop)
+
+ def test_happy_path_renders_name_and_id_per_line(self):
+ self.connector.list_locks.return_value = [
+ {"id": "31", "name": "101"},
+ {"id": "32", "name": "102"},
+ ]
+
+ action = self.vendor.action_fetch_locks()
+
+ self.assertEqual(action["res_model"], "lock.list.wizard")
+ self.assertEqual(action["target"], "new")
+ wizard = self.env["lock.list.wizard"].browse(action["res_id"])
+ self.assertEqual(wizard.vendor_id, self.vendor)
+ self.assertEqual(wizard.lock_listing, "101\t31\n102\t32")
+
+ def test_empty_listing_shows_placeholder(self):
+ self.connector.list_locks.return_value = []
+
+ action = self.vendor.action_fetch_locks()
+
+ wizard = self.env["lock.list.wizard"].browse(action["res_id"])
+ self.assertEqual(wizard.lock_listing, "No locks returned.")
+
+ def test_not_implemented_is_caught_as_user_error(self):
+ # A vendor whose connector inherits the base default must surface a
+ # friendly message, not a raw NotImplementedError traceback.
+ self.connector.list_locks.side_effect = NotImplementedError()
+
+ with self.assertRaises(UserError):
+ self.vendor.action_fetch_locks()
diff --git a/pms_smartlock_base/tests/test_lock_code_sync.py b/pms_smartlock_base/tests/test_lock_code_sync.py
index 94528845..b618615a 100644
--- a/pms_smartlock_base/tests/test_lock_code_sync.py
+++ b/pms_smartlock_base/tests/test_lock_code_sync.py
@@ -214,6 +214,7 @@ def test_happy_path_revokes_and_marks_cancelled(self):
self.code._sync_remove()
self.connector.revoke_access.assert_called_once_with(
grant_ref=self.code.vendor_grant_ref,
+ pin=self.code.pin,
)
# The cancelled flag gets written **after** vendor confirmation
# — the safety-first invariant from the design memo.
diff --git a/pms_smartlock_base/views/lock_vendor_views.xml b/pms_smartlock_base/views/lock_vendor_views.xml
index 78514a1e..96cf098e 100644
--- a/pms_smartlock_base/views/lock_vendor_views.xml
+++ b/pms_smartlock_base/views/lock_vendor_views.xml
@@ -21,6 +21,15 @@
lock.vendor