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
+
+
+ + + + lock.list.wizard.form + lock.list.wizard + + + + + + + + +