Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pms_smartlock_base/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
28 changes: 28 additions & 0 deletions pms_smartlock_base/models/lock_vendor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pms_smartlock_base/security/ir.model.access.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pms_smartlock_base/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
52 changes: 52 additions & 0 deletions pms_smartlock_base/tests/test_fetch_locks.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions pms_smartlock_base/tests/test_lock_code_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions pms_smartlock_base/views/lock_vendor_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@
<field name="model">lock.vendor</field>
<field name="arch" type="xml">
<form string="Lock Vendor">
<header>
<button
name="action_fetch_locks"
type="object"
string="Fetch Locks"
class="btn-primary"
attrs="{'invisible': [('vendor_type', '=', False)]}"
/>
</header>
<sheet>
<widget
name="web_ribbon"
Expand Down
1 change: 1 addition & 0 deletions pms_smartlock_base/wizards/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import lock_code_pin_viewer
from . import lock_list_wizard
17 changes: 17 additions & 0 deletions pms_smartlock_base/wizards/lock_list_wizard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from odoo import fields, models


class LockListWizard(models.TransientModel):
_name = "lock.list.wizard"
_description = "Smartlock Listing"

vendor_id = fields.Many2one(
comodel_name="lock.vendor",
readonly=True,
)
lock_listing = fields.Text(
string="Locks",
readonly=True,
help="Plain-text list of the vendor's locks (name and device id, one "
"per line) so the operator can read the ids needed to configure rooms.",
)
17 changes: 17 additions & 0 deletions pms_smartlock_base/wizards/lock_list_wizard_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>

<record id="lock_list_wizard_view_form" model="ir.ui.view">
<field name="name">lock.list.wizard.form</field>
<field name="model">lock.list.wizard</field>
<field name="arch" type="xml">
<form string="Locks">
<field name="lock_listing" readonly="1" nolabel="1" widget="text" />
<footer>
<button string="Close" class="btn-secondary" special="cancel" />
</footer>
</form>
</field>
</record>

</odoo>
Loading