lambda-erp 0.8.27__tar.gz → 0.8.29__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/PKG-INFO +1 -1
  2. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/chat.py +31 -25
  3. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/routers/mcp.py +5 -28
  4. lambda_erp-0.8.29/api/tool_permissions.py +52 -0
  5. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/pyproject.toml +1 -1
  6. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/.gitignore +0 -0
  7. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/LICENSE +0 -0
  8. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/README.md +0 -0
  9. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/__init__.py +0 -0
  10. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/attachments.py +0 -0
  11. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/auth.py +0 -0
  12. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/bootstrap.py +0 -0
  13. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/demo_limits.py +0 -0
  14. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/deps.py +0 -0
  15. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/errors.py +0 -0
  16. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/list_values.py +0 -0
  17. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/main.py +0 -0
  18. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/oauth.py +0 -0
  19. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/pdf.py +0 -0
  20. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/providers.py +0 -0
  21. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/remarks_md.py +0 -0
  22. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/routers/__init__.py +0 -0
  23. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/routers/accounting.py +0 -0
  24. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/routers/actions.py +0 -0
  25. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/routers/admin.py +0 -0
  26. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/routers/analytics.py +0 -0
  27. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/routers/availability.py +0 -0
  28. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/routers/bank_reconciliation.py +0 -0
  29. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/routers/bank_statements.py +0 -0
  30. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/routers/chat_api.py +0 -0
  31. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/routers/documents.py +0 -0
  32. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/routers/masters.py +0 -0
  33. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/routers/proposals.py +0 -0
  34. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/routers/reports.py +0 -0
  35. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/routers/setup.py +0 -0
  36. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/services.py +0 -0
  37. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/templates/document.html +0 -0
  38. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/api/templates/proposal.html +0 -0
  39. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/docs/agents/README.md +0 -0
  40. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/frontend/LICENSE +0 -0
  41. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/frontend/README.md +0 -0
  42. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/frontend/src/api/client.ts +0 -0
  43. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/__init__.py +0 -0
  44. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/__init__.py +0 -0
  45. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/bank_account.py +0 -0
  46. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/bank_reconciliation.py +0 -0
  47. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/bank_statement_import.py +0 -0
  48. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/bank_transaction.py +0 -0
  49. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/budget.py +0 -0
  50. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/camt.py +0 -0
  51. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  52. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/general_ledger.py +0 -0
  53. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/journal_entry.py +0 -0
  54. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/payment_entry.py +0 -0
  55. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/pos_invoice.py +0 -0
  56. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/purchase_invoice.py +0 -0
  57. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/revaluation.py +0 -0
  58. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/sales_invoice.py +0 -0
  59. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/__init__.py +0 -0
  60. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/engine.py +0 -0
  61. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/pack.py +0 -0
  62. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
  63. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/ch.py +0 -0
  64. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
  65. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
  66. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
  67. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/packs/generic.py +0 -0
  68. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/profiles.py +0 -0
  69. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/setup/spine.py +0 -0
  70. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/accounting/subscription.py +0 -0
  71. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/assets/__init__.py +0 -0
  72. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/assets/asset.py +0 -0
  73. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/assets/reservation.py +0 -0
  74. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/buying/__init__.py +0 -0
  75. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/buying/purchase_order.py +0 -0
  76. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/controllers/__init__.py +0 -0
  77. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/controllers/currency.py +0 -0
  78. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/controllers/defaults.py +0 -0
  79. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/controllers/pricing_rule.py +0 -0
  80. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  81. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/database.py +0 -0
  82. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/exceptions.py +0 -0
  83. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/hooks.py +0 -0
  84. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/model.py +0 -0
  85. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/selling/__init__.py +0 -0
  86. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/selling/proposal.py +0 -0
  87. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/selling/quotation.py +0 -0
  88. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/selling/sales_order.py +0 -0
  89. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/simulation.py +0 -0
  90. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/stock/__init__.py +0 -0
  91. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/stock/delivery_note.py +0 -0
  92. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/stock/purchase_receipt.py +0 -0
  93. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/stock/stock_entry.py +0 -0
  94. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/stock/stock_ledger.py +0 -0
  95. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/lambda_erp/utils.py +0 -0
  96. {lambda_erp-0.8.27 → lambda_erp-0.8.29}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lambda-erp
3
- Version: 0.8.27
3
+ Version: 0.8.29
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: Apache-2.0
@@ -27,6 +27,7 @@ from fastapi import APIRouter, Depends as _Depends, HTTPException, WebSocket, We
27
27
  from openai import OpenAI
28
28
 
29
29
  from api import services
30
+ from api.tool_permissions import tool_allowed, tool_permission_error
30
31
  from api.demo_limits import (
31
32
  demo_call_reserve_usd,
32
33
  demo_max_completion_tokens,
@@ -899,17 +900,16 @@ TOOLS = [
899
900
  "type": "function",
900
901
  "function": {
901
902
  "name": "reconcile_bank_transaction",
902
- "description": "Reconcile imported bank transactions after explicit user confirmation. mode=invoice_payment creates and submits a Payment Entry allocated to one or more invoices; mode=existing_voucher links an already-submitted Payment Entry or Journal Entry with an exactly matching bank leg. If suggest_bank_reconciliation returns an existing_voucher_group, pass every exact bank transaction it lists in bank_transactions; the complete group is linked atomically. mode=journal creates and submits a two-sided Journal Entry against a non-bank, non-AR/AP account. This posts or links real accounting data. Never call in the same turn as the first proposal; confirmed must reflect a later explicit confirmation.",
903
+ "description": "Reconcile imported bank transactions after explicit user confirmation. Always pass bank_transactions: use a one-item list for an individual reconciliation and the complete list returned by an existing_voucher_group suggestion for a grouped reconciliation. mode=invoice_payment creates and submits a Payment Entry allocated to one or more invoices; mode=existing_voucher links an already-submitted Payment Entry or Journal Entry with an exactly matching bank leg, atomically for a group; mode=journal creates and submits a two-sided Journal Entry against a non-bank, non-AR/AP account. invoice_payment and journal accept exactly one bank transaction. This posts or links real accounting data. Never call in the same turn as the first proposal; confirmed must reflect a later explicit confirmation.",
903
904
  "parameters": {
904
905
  "type": "object",
905
906
  "properties": {
906
- "bank_transaction": {"type": "string"},
907
907
  "bank_transactions": {
908
908
  "type": "array",
909
909
  "items": {"type": "string"},
910
- "minItems": 2,
910
+ "minItems": 1,
911
911
  "maxItems": 100,
912
- "description": "For an existing_voucher_group only: all exact Bank Transaction names returned by the suggestion, including bank_transaction.",
912
+ "description": "Bank Transaction names to reconcile: exactly one for an individual match/payment/journal, or the complete exact group returned by the suggestion.",
913
913
  },
914
914
  "mode": {"type": "string", "enum": ["invoice_payment", "existing_voucher", "journal"]},
915
915
  "allocations": {
@@ -931,7 +931,7 @@ TOOLS = [
931
931
  "remarks": {"type": "string"},
932
932
  "confirmed": {"type": "boolean"},
933
933
  },
934
- "required": ["bank_transaction", "mode", "confirmed"],
934
+ "required": ["bank_transactions", "mode", "confirmed"],
935
935
  },
936
936
  },
937
937
  },
@@ -1439,10 +1439,12 @@ def build_tools(user_info: dict | None = None):
1439
1439
  role = user_info.get("role")
1440
1440
  action_tools = [
1441
1441
  tool for tool in action_tools
1442
- if services.registered_action_allowed(tool["function"]["name"], role)
1442
+ if tool_allowed(tool["function"]["name"], role)
1443
1443
  ]
1444
- if role not in {"manager", "admin"}:
1445
- hidden_tools.add("import_bank_statement_attachments")
1444
+ hidden_tools = {
1445
+ tool["function"]["name"] for tool in TOOLS
1446
+ if not tool_allowed(tool["function"]["name"], role)
1447
+ }
1446
1448
  if not extra_docs and not extra_masters and not action_tools and not hidden_tools:
1447
1449
  return TOOLS
1448
1450
  tools = copy.deepcopy(TOOLS)
@@ -2083,35 +2085,32 @@ def _handle_reconcile_bank_transaction(args, user_info: dict | None = None):
2083
2085
  )
2084
2086
  }
2085
2087
  from lambda_erp.accounting.bank_reconciliation import (
2086
- reconcile_with_existing_voucher,
2087
2088
  reconcile_with_existing_voucher_group,
2088
2089
  reconcile_with_journal,
2089
2090
  reconcile_with_payment,
2090
2091
  )
2091
2092
  user = (user_info or {}).get("name")
2092
2093
  mode = args.get("mode")
2094
+ bank_transactions = args.get("bank_transactions")
2095
+ if not isinstance(bank_transactions, list) or not bank_transactions:
2096
+ return {"error": "bank_transactions must contain at least one Bank Transaction name"}
2097
+ if mode in {"invoice_payment", "journal"} and len(bank_transactions) != 1:
2098
+ return {"error": f"mode={mode} requires exactly one bank transaction"}
2099
+ bank_transaction = bank_transactions[0]
2093
2100
  try:
2094
2101
  if mode == "invoice_payment":
2095
2102
  return reconcile_with_payment(
2096
- args.get("bank_transaction"), args.get("allocations") or [],
2103
+ bank_transaction, args.get("allocations") or [],
2097
2104
  conversion_rate=args.get("conversion_rate"), user=user, confirmed=True,
2098
2105
  )
2099
2106
  if mode == "existing_voucher":
2100
- group = args.get("bank_transactions")
2101
- if group:
2102
- if args.get("bank_transaction") not in group:
2103
- return {"error": "bank_transactions must include bank_transaction"}
2104
- return reconcile_with_existing_voucher_group(
2105
- group, args.get("voucher_type"), args.get("voucher_no"),
2106
- user=user, confirmed=True,
2107
- )
2108
- return reconcile_with_existing_voucher(
2109
- args.get("bank_transaction"), args.get("voucher_type"), args.get("voucher_no"),
2107
+ return reconcile_with_existing_voucher_group(
2108
+ bank_transactions, args.get("voucher_type"), args.get("voucher_no"),
2110
2109
  user=user, confirmed=True,
2111
2110
  )
2112
2111
  if mode == "journal":
2113
2112
  return reconcile_with_journal(
2114
- args.get("bank_transaction"), args.get("counterparty_account"),
2113
+ bank_transaction, args.get("counterparty_account"),
2115
2114
  conversion_rate=args.get("conversion_rate"), remarks=args.get("remarks"),
2116
2115
  user=user, confirmed=True,
2117
2116
  )
@@ -2573,8 +2572,9 @@ one transaction or every transaction in an exact `existing_voucher_group`,
2573
2572
  voucher changes the GL and may change invoice outstanding amounts. Ask for explicit confirmation in
2574
2573
  a later user turn, then call `reconcile_bank_transaction` with `confirmed=true`. Clearly disclose any
2575
2574
  unallocated on-account remainder. Disclose all group members and their total before asking for
2576
- confirmation; for a group, copy the complete
2577
- `bank_transactions` list from the suggestion into the reconciliation call and never add or omit IDs.
2575
+ confirmation. Every reconciliation call uses `bank_transactions`: pass a one-item list for an
2576
+ individual reconciliation; for a group, copy the complete list from the suggestion and never add or
2577
+ omit IDs.
2578
2578
  Never match a Bank Transaction directly to an invoice because that
2579
2579
  would omit the cash posting. For reversals, first explain whether the voucher was created by the
2580
2580
  reconciliation (it will be cancelled and reversed) or merely linked (it will only be unlinked), and
@@ -3559,7 +3559,8 @@ async def run_thinking_loop(
3559
3559
  # If GPT is delegating report code-gen to the Anthropic specialist,
3560
3560
  # surface the handoff in the UI.
3561
3561
  will_delegate_to_code_specialist = (
3562
- fn_name in ("create_custom_analytics_report", "update_custom_analytics_report")
3562
+ tool_allowed(fn_name, user_role)
3563
+ and fn_name in ("create_custom_analytics_report", "update_custom_analytics_report")
3563
3564
  and not fn_args.get("transform_js")
3564
3565
  and not fn_args.get("data_requests")
3565
3566
  )
@@ -3572,7 +3573,12 @@ async def run_thinking_loop(
3572
3573
  })
3573
3574
 
3574
3575
  handler = tool_handlers.get(fn_name)
3575
- if not handler:
3576
+ # The model's tool list and prompt are not authorization controls.
3577
+ # Check the caller at execution even for invented or replayed calls.
3578
+ if not tool_allowed(fn_name, user_role):
3579
+ result = tool_permission_error(fn_name, user_role)
3580
+ success = False
3581
+ elif not handler:
3576
3582
  result = {"error": f"Unknown tool: {fn_name}"}
3577
3583
  success = False
3578
3584
  else:
@@ -25,6 +25,7 @@ from api.auth import get_current_user
25
25
  from api import services
26
26
  from api import chat as chat_mod
27
27
  from api.chat import TOOL_HANDLERS, build_tools
28
+ from api.tool_permissions import tool_allowed
28
29
 
29
30
  router = APIRouter(tags=["mcp"])
30
31
 
@@ -42,37 +43,10 @@ _EXCLUDE = {
42
43
  "plan_company_setup",
43
44
  "apply_company_setup",
44
45
  }
45
- # Mirror the REST permission model: reads are viewer+, writes are manager+,
46
- # delete_master is admin-only (the handler also re-checks).
47
- _WRITE = {
48
- "create_document", "update_document", "batch_update_documents",
49
- "submit_document", "cancel_document",
50
- "discard_document", "convert_document", "create_master", "update_master",
51
- "reconcile_bank_transaction", "undo_bank_reconciliation",
52
- }
53
- _ADMIN = {"delete_master"}
54
- _MANAGER_ONLY = {
55
- "list_bank_reconciliation_queue", "suggest_bank_reconciliation",
56
- "reconcile_bank_transaction", "undo_bank_reconciliation",
57
- }
58
-
59
-
60
- def _can_write(role) -> bool:
61
- return role in ("manager", "admin", "public_manager")
62
46
 
63
47
 
64
48
  def _allowed(name: str, role) -> bool:
65
- if name in _EXCLUDE:
66
- return False
67
- if name in services.REGISTERED_ACTIONS:
68
- return services.registered_action_allowed(name, role)
69
- if name in _ADMIN:
70
- return role == "admin"
71
- if name in _MANAGER_ONLY:
72
- return role in ("manager", "admin")
73
- if name in _WRITE:
74
- return _can_write(role)
75
- return True
49
+ return name not in _EXCLUDE and tool_allowed(name, role)
76
50
 
77
51
 
78
52
  def _require_caller(request: Request) -> dict:
@@ -99,6 +73,9 @@ def _tools(role) -> list:
99
73
 
100
74
 
101
75
  def _call(name: str, args: dict, user: dict):
76
+ # Preserve MCP's unknown-tool error without dispatching an unclassified tool.
77
+ if name not in TOOL_HANDLERS and name != "delete_master" and name not in services.REGISTERED_ACTIONS:
78
+ raise KeyError(name)
102
79
  role = user.get("role")
103
80
  if not _allowed(name, role):
104
81
  return {"error": f"'{name}' is not available to a {role or 'viewer'} key."}
@@ -0,0 +1,52 @@
1
+ """Shared execution policy for ERP tools exposed through chat and MCP.
2
+
3
+ Every built-in tool must be classified here. Unknown tools and unknown callers
4
+ are denied; registered extension actions carry their own explicit policy.
5
+ Session ownership and other resource checks still belong to their handlers.
6
+ """
7
+
8
+ from api import services
9
+
10
+
11
+ _READERS = frozenset({"viewer", "manager", "admin", "public_manager"})
12
+ _DOCUMENT_WRITERS = frozenset({"manager", "admin", "public_manager"})
13
+ _MANAGERS = frozenset({"manager", "admin"})
14
+ _ADMINS = frozenset({"admin"})
15
+
16
+ TOOL_ROLES = {
17
+ **dict.fromkeys((
18
+ "list_documents", "get_document_fields", "get_document",
19
+ "get_master_fields", "search_masters", "get_report", "get_current_time",
20
+ "retrieve_chat_history", "list_chat_attachments", "retrieve_chat_attachment",
21
+ "preview_bank_statement_attachments", "query_dataset",
22
+ # Personal report drafts follow the viewer-accessible reporting API.
23
+ # Their handlers enforce ownership; these do not post ERP transactions.
24
+ "create_custom_analytics_report", "get_custom_analytics_report",
25
+ "update_custom_analytics_report",
26
+ ), _READERS),
27
+ **dict.fromkeys((
28
+ "create_document", "update_document", "batch_update_documents",
29
+ "submit_document", "cancel_document", "discard_document", "convert_document",
30
+ ), _DOCUMENT_WRITERS),
31
+ **dict.fromkeys((
32
+ "create_master", "update_master", "revalue_currencies",
33
+ "import_bank_statement_attachments", "list_bank_reconciliation_queue",
34
+ "suggest_bank_reconciliation", "reconcile_bank_transaction",
35
+ "undo_bank_reconciliation",
36
+ ), _MANAGERS),
37
+ **dict.fromkeys((
38
+ "delete_master", "plan_company_setup", "apply_company_setup",
39
+ ), _ADMINS),
40
+ }
41
+
42
+
43
+ def tool_allowed(name: str, role: str | None) -> bool:
44
+ if role not in _READERS:
45
+ return False
46
+ if name in TOOL_ROLES:
47
+ return role in TOOL_ROLES[name]
48
+ return services.registered_action_allowed(name, role)
49
+
50
+
51
+ def tool_permission_error(name: str, role: str | None) -> dict:
52
+ return {"error": f"Tool '{name}' is not available to role '{role or 'unauthenticated'}'."}
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.8.27"
3
+ version = "0.8.29"
4
4
  description = "Core ERP logic - accounting, sales, purchasing, inventory"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes