fix: resolve STT AttributeError and int/str TypeError in teamsbot service
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
7778325e5e
commit
2d7da8a66d
5 changed files with 54 additions and 286 deletions
|
|
@ -490,7 +490,7 @@ def _getEffectiveConfig(instanceId: str, userId: str, interface) -> TeamsbotConf
|
|||
overrides[field] = value
|
||||
|
||||
if overrides:
|
||||
return baseConfig.model_copy(update=overrides)
|
||||
return TeamsbotConfig.model_validate({**baseConfig.model_dump(), **overrides})
|
||||
return baseConfig
|
||||
|
||||
|
||||
|
|
@ -1228,7 +1228,7 @@ async def botWebsocket(
|
|||
if value is not None:
|
||||
overrides[field] = value
|
||||
if overrides:
|
||||
config = config.model_copy(update=overrides)
|
||||
config = TeamsbotConfig.model_validate({**config.model_dump(), **overrides})
|
||||
logger.info(f"Browser Bot WebSocket: Applied user settings overrides: {list(overrides.keys())}")
|
||||
|
||||
service = TeamsbotService(originalUser, mandateId, instanceId, config)
|
||||
|
|
|
|||
|
|
@ -400,38 +400,30 @@ class TeamsbotService:
|
|||
if len(audioBytes) < 1000:
|
||||
return
|
||||
|
||||
# Use the existing Google Cloud Speech connector for STT
|
||||
speechConnector = voiceInterface.getSpeechConnector() if voiceInterface else None
|
||||
if not speechConnector or not hasattr(speechConnector, 'speech_client'):
|
||||
logger.warning(f"[AudioChunk] No speech client available for session {sessionId}")
|
||||
if not voiceInterface:
|
||||
logger.warning(f"[AudioChunk] No voice interface available for session {sessionId}")
|
||||
return
|
||||
|
||||
from google.cloud import speech
|
||||
config = speech.RecognitionConfig(
|
||||
encoding=speech.RecognitionConfig.AudioEncoding.LINEAR16,
|
||||
sample_rate_hertz=sampleRate,
|
||||
language_code=self.config.language or "de-DE",
|
||||
enable_automatic_punctuation=True,
|
||||
sttResult = await voiceInterface.speechToText(
|
||||
audioContent=audioBytes,
|
||||
language=self.config.language or "de-DE",
|
||||
sampleRate=sampleRate,
|
||||
)
|
||||
audio = speech.RecognitionAudio(content=audioBytes)
|
||||
|
||||
response = speechConnector.speech_client.recognize(config=config, audio=audio)
|
||||
|
||||
for result in response.results:
|
||||
if result.alternatives:
|
||||
text = result.alternatives[0].transcript.strip()
|
||||
if text:
|
||||
logger.info(f"[AudioChunk] STT result: {text[:80]}...")
|
||||
await self._processTranscript(
|
||||
sessionId=sessionId,
|
||||
speaker="Meeting Audio",
|
||||
text=text,
|
||||
isFinal=True,
|
||||
interface=interface,
|
||||
voiceInterface=voiceInterface,
|
||||
websocket=websocket,
|
||||
source="audioCapture",
|
||||
)
|
||||
if sttResult and sttResult.get("success") and sttResult.get("text"):
|
||||
text = sttResult["text"].strip()
|
||||
if text:
|
||||
logger.info(f"[AudioChunk] STT result: {text[:80]}...")
|
||||
await self._processTranscript(
|
||||
sessionId=sessionId,
|
||||
speaker="Meeting Audio",
|
||||
text=text,
|
||||
isFinal=True,
|
||||
interface=interface,
|
||||
voiceInterface=voiceInterface,
|
||||
websocket=websocket,
|
||||
source="audioCapture",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[AudioChunk] STT error for session {sessionId}: {type(e).__name__}: {e}")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# All rights reserved.
|
||||
"""Trustee models: TrusteeOrganisation, TrusteeRole, TrusteeAccess, TrusteeContract, TrusteeDocument, TrusteePosition, TrusteePositionDocument."""
|
||||
"""Trustee models: TrusteeOrganisation, TrusteeRole, TrusteeAccess, TrusteeContract, TrusteeDocument, TrusteePosition."""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
|
@ -384,9 +384,8 @@ registerModelLabels(
|
|||
class TrusteePosition(BaseModel):
|
||||
"""Contains booking positions (expense entries).
|
||||
|
||||
Note: organisationId and contractId removed as per architecture decision:
|
||||
- The feature instance IS the organisation
|
||||
- Contracts are eliminated from the model
|
||||
Each position references exactly one source document via documentId (1:N relationship).
|
||||
One document (e.g. bank statement) can generate many positions.
|
||||
"""
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
|
|
@ -397,6 +396,16 @@ class TrusteePosition(BaseModel):
|
|||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
documentId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Reference to TrusteeDocument.id (source document that generated this position)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": False,
|
||||
"frontend_options": "/api/trustee/{instanceId}/documents/options"
|
||||
}
|
||||
)
|
||||
valuta: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Value date (ISO format: YYYY-MM-DD)",
|
||||
|
|
@ -538,6 +547,7 @@ registerModelLabels(
|
|||
{"en": "Position", "fr": "Position", "de": "Position"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||
"documentId": {"en": "Document", "fr": "Document", "de": "Dokument"},
|
||||
"valuta": {"en": "Value Date", "fr": "Date de valeur", "de": "Valutadatum"},
|
||||
"transactionDateTime": {"en": "Transaction Date/Time", "fr": "Date/Heure de transaction", "de": "Transaktionszeitpunkt"},
|
||||
"company": {"en": "Company", "fr": "Entreprise", "de": "Firma"},
|
||||
|
|
@ -555,71 +565,3 @@ registerModelLabels(
|
|||
)
|
||||
|
||||
|
||||
class TrusteePositionDocument(BaseModel):
|
||||
"""Cross-reference table linking positions to documents (many-to-many).
|
||||
|
||||
Note: organisationId and contractId removed as per architecture decision:
|
||||
- The feature instance IS the organisation
|
||||
- Contracts are eliminated from the model
|
||||
"""
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
description="Unique link ID",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
documentId: str = Field(
|
||||
description="Reference to TrusteeDocument.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "/api/trustee/{instanceId}/documents/options"
|
||||
}
|
||||
)
|
||||
positionId: str = Field(
|
||||
description="Reference to TrusteePosition.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "/api/trustee/{instanceId}/positions/options"
|
||||
}
|
||||
)
|
||||
mandateId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Mandate ID (auto-set from context)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False,
|
||||
"frontend_hidden": True
|
||||
}
|
||||
)
|
||||
featureInstanceId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Feature Instance ID for instance-level isolation (auto-set from context)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False,
|
||||
"frontend_hidden": True
|
||||
}
|
||||
)
|
||||
# System attributes are automatically set by DatabaseConnector
|
||||
|
||||
|
||||
registerModelLabels(
|
||||
"TrusteePositionDocument",
|
||||
{"en": "Position-Document Link", "fr": "Lien Position-Document", "de": "Position-Dokument-Verknüpfung"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||
"documentId": {"en": "Document", "fr": "Document", "de": "Dokument"},
|
||||
"positionId": {"en": "Position", "fr": "Position", "de": "Position"},
|
||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ from .datamodelFeatureTrustee import (
|
|||
TrusteeContract,
|
||||
TrusteeDocument,
|
||||
TrusteePosition,
|
||||
TrusteePositionDocument,
|
||||
)
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResult
|
||||
|
||||
|
|
@ -1107,10 +1106,8 @@ class TrusteeObjects:
|
|||
def deleteDocument(self, documentId: str) -> bool:
|
||||
"""Delete a document.
|
||||
|
||||
All position-document cross-table entries (TrusteePositionDocument) referencing
|
||||
this document are deleted first, then the document.
|
||||
Positions referencing this document will have their documentId set to None.
|
||||
"""
|
||||
# Get existing document to check creator
|
||||
existingRecords = self.db.getRecordset(TrusteeDocument, recordFilter={"id": documentId})
|
||||
existing = existingRecords[0] if existingRecords else None
|
||||
|
||||
|
|
@ -1120,12 +1117,17 @@ class TrusteeObjects:
|
|||
|
||||
createdBy = existing.get("_createdBy")
|
||||
|
||||
# Check system RBAC permission (userreport can only delete their own records)
|
||||
if not self.checkCombinedPermission(TrusteeDocument, "delete", recordCreatedBy=createdBy):
|
||||
logger.warning(f"User {self.userId} lacks permission to delete document")
|
||||
return False
|
||||
|
||||
self._deletePositionDocumentLinksForDocument(documentId)
|
||||
# Clear documentId on positions that reference this document
|
||||
positions = self.db.getRecordset(TrusteePosition, recordFilter={"documentId": documentId})
|
||||
for pos in positions:
|
||||
posId = pos.get("id")
|
||||
if posId:
|
||||
self.db.recordModify(TrusteePosition, posId, {"documentId": None})
|
||||
|
||||
return self.db.recordDelete(TrusteeDocument, documentId)
|
||||
|
||||
# ===== Position CRUD =====
|
||||
|
|
@ -1276,12 +1278,7 @@ class TrusteeObjects:
|
|||
return TrusteePosition(**{k: v for k, v in updatedRecord.items() if not k.startswith("_")})
|
||||
|
||||
def deletePosition(self, positionId: str) -> bool:
|
||||
"""Delete a position.
|
||||
|
||||
All position-document cross-table entries (TrusteePositionDocument) referencing
|
||||
this position are deleted first, then the position.
|
||||
"""
|
||||
# Get existing position to check creator
|
||||
"""Delete a position."""
|
||||
existingRecords = self.db.getRecordset(TrusteePosition, recordFilter={"id": positionId})
|
||||
existing = existingRecords[0] if existingRecords else None
|
||||
|
||||
|
|
@ -1291,177 +1288,27 @@ class TrusteeObjects:
|
|||
|
||||
createdBy = existing.get("_createdBy")
|
||||
|
||||
# Check system RBAC permission (userreport can only delete their own records)
|
||||
if not self.checkCombinedPermission(TrusteePosition, "delete", recordCreatedBy=createdBy):
|
||||
logger.warning(f"User {self.userId} lacks permission to delete position")
|
||||
return False
|
||||
|
||||
self._deletePositionDocumentLinksForPosition(positionId)
|
||||
return self.db.recordDelete(TrusteePosition, positionId)
|
||||
|
||||
# ===== Position-Document Link CRUD =====
|
||||
# ===== Position-Document Queries =====
|
||||
|
||||
def createPositionDocument(self, data: Dict[str, Any]) -> Optional[TrusteePositionDocument]:
|
||||
"""Create a new position-document link.
|
||||
|
||||
Note: organisationId and contractId removed - feature instance IS the organisation.
|
||||
Permission is checked via system RBAC (feature-level access).
|
||||
"""
|
||||
# Check system RBAC permission
|
||||
if not self.checkCombinedPermission(TrusteePositionDocument, "create"):
|
||||
logger.warning(f"User {self.userId} lacks permission to create position-document link")
|
||||
return None
|
||||
|
||||
# Auto-set context fields
|
||||
data["mandateId"] = self.mandateId
|
||||
data["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
import uuid
|
||||
linkId = data.get("id") or str(uuid.uuid4())
|
||||
data["id"] = linkId
|
||||
|
||||
createdRecord = self.db.recordCreate(TrusteePositionDocument, data)
|
||||
if createdRecord and createdRecord.get("id"):
|
||||
return TrusteePositionDocument(**{k: v for k, v in createdRecord.items() if not k.startswith("_")})
|
||||
return None
|
||||
|
||||
def getPositionDocument(self, linkId: str) -> Optional[TrusteePositionDocument]:
|
||||
"""Get a single position-document link by ID."""
|
||||
records = self.db.getRecordset(TrusteePositionDocument, recordFilter={"id": linkId})
|
||||
if not records:
|
||||
return None
|
||||
return TrusteePositionDocument(**{k: v for k, v in records[0].items() if not k.startswith("_")})
|
||||
|
||||
def updatePositionDocument(self, linkId: str, data: Dict[str, Any]) -> Optional[TrusteePositionDocument]:
|
||||
"""Update a position-document link."""
|
||||
# Check permission
|
||||
if not self.checkCombinedPermission(TrusteePositionDocument, "update"):
|
||||
logger.warning(f"User {self.userId} lacks permission to update position-document link")
|
||||
return None
|
||||
|
||||
# Verify link exists and belongs to this instance
|
||||
existing = self.db.getRecordset(TrusteePositionDocument, recordFilter={"id": linkId})
|
||||
if not existing:
|
||||
logger.warning(f"Position-document link {linkId} not found")
|
||||
return None
|
||||
|
||||
existingRecord = existing[0]
|
||||
if existingRecord.get("featureInstanceId") != self.featureInstanceId:
|
||||
logger.warning(f"Link {linkId} belongs to different instance")
|
||||
return None
|
||||
|
||||
# Prevent changing context fields
|
||||
data.pop("id", None)
|
||||
data.pop("mandateId", None)
|
||||
data.pop("featureInstanceId", None)
|
||||
|
||||
updatedRecord = self.db.recordModify(TrusteePositionDocument, linkId, data)
|
||||
if updatedRecord and updatedRecord.get("id"):
|
||||
return TrusteePositionDocument(**{k: v for k, v in updatedRecord.items() if not k.startswith("_")})
|
||||
return None
|
||||
|
||||
def getAllPositionDocuments(self, params: Optional[PaginationParams] = None) -> PaginatedResult:
|
||||
"""Get all position-document links with RBAC filtering + feature-level access filtering."""
|
||||
# Step 1: System RBAC filtering with per-row permissions
|
||||
def getPositionsByDocument(self, documentId: str) -> List[TrusteePosition]:
|
||||
"""Get all positions that reference a specific document (1:N via documentId FK)."""
|
||||
records = getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteePositionDocument,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter=None,
|
||||
orderBy="id",
|
||||
mandateId=self.mandateId,
|
||||
featureInstanceId=self.featureInstanceId,
|
||||
enrichPermissions=True,
|
||||
featureCode=self.FEATURE_CODE
|
||||
)
|
||||
|
||||
totalItems = len(records)
|
||||
if params:
|
||||
pageSize = params.pageSize or 20
|
||||
page = params.page or 1
|
||||
startIdx = (page - 1) * pageSize
|
||||
endIdx = startIdx + pageSize
|
||||
items = records[startIdx:endIdx]
|
||||
totalPages = math.ceil(totalItems / pageSize) if pageSize > 0 else 1
|
||||
else:
|
||||
items = records
|
||||
totalPages = 1
|
||||
page = 1
|
||||
pageSize = totalItems
|
||||
|
||||
return PaginatedResult(
|
||||
items=items,
|
||||
totalItems=totalItems,
|
||||
totalPages=totalPages
|
||||
)
|
||||
|
||||
def getDocumentsForPosition(self, positionId: str) -> List[TrusteePositionDocument]:
|
||||
"""Get all documents linked to a position."""
|
||||
# Step 1: System RBAC filtering
|
||||
links = getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteePositionDocument,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter={"positionId": positionId},
|
||||
orderBy="id",
|
||||
mandateId=self.mandateId,
|
||||
featureInstanceId=self.featureInstanceId,
|
||||
featureCode=self.FEATURE_CODE
|
||||
)
|
||||
return [TrusteePositionDocument(**{k: v for k, v in r.items() if not k.startswith("_")}) for r in links]
|
||||
|
||||
def getPositionsForDocument(self, documentId: str) -> List[TrusteePositionDocument]:
|
||||
"""Get all positions linked to a document."""
|
||||
# Step 1: System RBAC filtering
|
||||
links = getRecordsetWithRBAC(
|
||||
connector=self.db,
|
||||
modelClass=TrusteePositionDocument,
|
||||
modelClass=TrusteePosition,
|
||||
currentUser=self.currentUser,
|
||||
recordFilter={"documentId": documentId},
|
||||
orderBy="id",
|
||||
orderBy="valuta",
|
||||
mandateId=self.mandateId,
|
||||
featureInstanceId=self.featureInstanceId,
|
||||
featureCode=self.FEATURE_CODE
|
||||
)
|
||||
return [TrusteePositionDocument(**{k: v for k, v in r.items() if not k.startswith("_")}) for r in links]
|
||||
|
||||
def deletePositionDocument(self, linkId: str) -> bool:
|
||||
"""Delete a position-document link.
|
||||
|
||||
Note: organisationId and contractId removed - feature instance IS the organisation.
|
||||
"""
|
||||
# Get existing link to check creator
|
||||
existingRecords = self.db.getRecordset(TrusteePositionDocument, recordFilter={"id": linkId})
|
||||
existing = existingRecords[0] if existingRecords else None
|
||||
|
||||
if not existing:
|
||||
logger.warning(f"Position-document link {linkId} not found")
|
||||
return False
|
||||
|
||||
createdBy = existing.get("_createdBy")
|
||||
|
||||
# Check system RBAC permission (userreport can only delete their own records)
|
||||
if not self.checkCombinedPermission(TrusteePositionDocument, "delete", recordCreatedBy=createdBy):
|
||||
logger.warning(f"User {self.userId} lacks permission to delete position-document link")
|
||||
return False
|
||||
|
||||
return self.db.recordDelete(TrusteePositionDocument, linkId)
|
||||
|
||||
def _deletePositionDocumentLinksForDocument(self, documentId: str) -> None:
|
||||
"""Delete all position-document cross-table entries referencing this document."""
|
||||
links = self.db.getRecordset(TrusteePositionDocument, recordFilter={"documentId": documentId})
|
||||
for link in links:
|
||||
linkId = link.get("id")
|
||||
if linkId:
|
||||
self.db.recordDelete(TrusteePositionDocument, linkId)
|
||||
|
||||
def _deletePositionDocumentLinksForPosition(self, positionId: str) -> None:
|
||||
"""Delete all position-document cross-table entries referencing this position."""
|
||||
links = self.db.getRecordset(TrusteePositionDocument, recordFilter={"positionId": positionId})
|
||||
for link in links:
|
||||
linkId = link.get("id")
|
||||
if linkId:
|
||||
self.db.recordDelete(TrusteePositionDocument, linkId)
|
||||
return [TrusteePosition(**{k: v for k, v in r.items() if not k.startswith("_")}) for r in records]
|
||||
|
||||
# ===== Trustee-specific Access Check =====
|
||||
|
||||
|
|
@ -1583,7 +1430,7 @@ class TrusteeObjects:
|
|||
|
||||
# operate role: CRUD for contracts, documents, positions
|
||||
if "operate" in roles:
|
||||
if modelClass in (TrusteeContract, TrusteeDocument, TrusteePosition, TrusteePositionDocument):
|
||||
if modelClass in (TrusteeContract, TrusteeDocument, TrusteePosition):
|
||||
return True
|
||||
# operate can read organisations
|
||||
if modelClass == TrusteeOrganisation and operation == "read":
|
||||
|
|
@ -1591,7 +1438,7 @@ class TrusteeObjects:
|
|||
|
||||
# userreport role: CRUD own records for documents/positions
|
||||
if "userreport" in roles:
|
||||
if modelClass in (TrusteeDocument, TrusteePosition, TrusteePositionDocument):
|
||||
if modelClass in (TrusteeDocument, TrusteePosition):
|
||||
# For create, always allowed
|
||||
if operation == "create":
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -33,11 +33,6 @@ UI_OBJECTS = [
|
|||
"label": {"en": "Documents", "de": "Dokumente", "fr": "Documents"},
|
||||
"meta": {"area": "documents"}
|
||||
},
|
||||
{
|
||||
"objectKey": "ui.feature.trustee.position-documents",
|
||||
"label": {"en": "Position Documents", "de": "Positions-Dokumente", "fr": "Documents de position"},
|
||||
"meta": {"area": "position-documents"}
|
||||
},
|
||||
{
|
||||
"objectKey": "ui.feature.trustee.expense-import",
|
||||
"label": {"en": "Expense Import", "de": "Spesen Import", "fr": "Import de dépenses"},
|
||||
|
|
@ -63,11 +58,6 @@ DATA_OBJECTS = [
|
|||
"label": {"en": "Document", "de": "Dokument", "fr": "Document"},
|
||||
"meta": {"table": "TrusteeDocument", "fields": ["id", "filename", "mimeType", "fileSize", "uploadDate"]}
|
||||
},
|
||||
{
|
||||
"objectKey": "data.feature.trustee.TrusteePositionDocument",
|
||||
"label": {"en": "Position-Document Assignment", "de": "Position-Dokument Zuordnung", "fr": "Assignation Position-Document"},
|
||||
"meta": {"table": "TrusteePositionDocument", "fields": ["id", "positionId", "documentId"]}
|
||||
},
|
||||
{
|
||||
"objectKey": "data.feature.trustee.*",
|
||||
"label": {"en": "All Trustee Data", "de": "Alle Treuhand-Daten", "fr": "Toutes les données fiduciaires"},
|
||||
|
|
@ -148,7 +138,6 @@ TEMPLATE_ROLES = [
|
|||
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
|
||||
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
|
||||
{"context": "UI", "item": "ui.feature.trustee.documents", "view": True},
|
||||
{"context": "UI", "item": "ui.feature.trustee.position-documents", "view": True},
|
||||
# Group-level DATA access
|
||||
{"context": "DATA", "item": None, "view": True, "read": "g", "create": "g", "update": "g", "delete": "g"},
|
||||
]
|
||||
|
|
@ -165,12 +154,10 @@ TEMPLATE_ROLES = [
|
|||
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
|
||||
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
|
||||
{"context": "UI", "item": "ui.feature.trustee.documents", "view": True},
|
||||
{"context": "UI", "item": "ui.feature.trustee.position-documents", "view": True},
|
||||
{"context": "UI", "item": "ui.feature.trustee.expense-import", "view": True},
|
||||
# Own records only (MY level) - explizite Regeln pro Tabelle
|
||||
# Own records only (MY level)
|
||||
{"context": "DATA", "item": "data.feature.trustee.TrusteePosition", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
|
||||
{"context": "DATA", "item": "data.feature.trustee.TrusteeDocument", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
|
||||
{"context": "DATA", "item": "data.feature.trustee.TrusteePositionDocument", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
|
||||
]
|
||||
},
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue