lambda-erp 0.3.3__tar.gz → 0.4.0__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 (83) hide show
  1. {lambda_erp-0.3.3/frontend → lambda_erp-0.4.0}/LICENSE +1 -0
  2. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/PKG-INFO +2 -1
  3. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/README.md +1 -0
  4. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/auth.py +84 -23
  5. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/pyproject.toml +1 -1
  6. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/.gitignore +0 -0
  7. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/__init__.py +0 -0
  8. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/attachments.py +0 -0
  9. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/bootstrap.py +0 -0
  10. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/chat.py +0 -0
  11. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/demo_limits.py +0 -0
  12. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/deps.py +0 -0
  13. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/errors.py +0 -0
  14. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/main.py +0 -0
  15. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/oauth.py +0 -0
  16. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/pdf.py +0 -0
  17. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/providers.py +0 -0
  18. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/remarks_md.py +0 -0
  19. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/routers/__init__.py +0 -0
  20. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/routers/accounting.py +0 -0
  21. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/routers/admin.py +0 -0
  22. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/routers/analytics.py +0 -0
  23. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/routers/bank_reconciliation.py +0 -0
  24. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/routers/chat_api.py +0 -0
  25. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/routers/documents.py +0 -0
  26. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/routers/masters.py +0 -0
  27. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/routers/proposals.py +0 -0
  28. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/routers/reports.py +0 -0
  29. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/routers/setup.py +0 -0
  30. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/services.py +0 -0
  31. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/templates/document.html +0 -0
  32. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/api/templates/proposal.html +0 -0
  33. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/docs/agents/README.md +0 -0
  34. {lambda_erp-0.3.3 → lambda_erp-0.4.0/frontend}/LICENSE +0 -0
  35. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/frontend/README.md +0 -0
  36. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/frontend/src/api/client.ts +0 -0
  37. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/__init__.py +0 -0
  38. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/__init__.py +0 -0
  39. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/bank_transaction.py +0 -0
  40. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/budget.py +0 -0
  41. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  42. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/general_ledger.py +0 -0
  43. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/journal_entry.py +0 -0
  44. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/payment_entry.py +0 -0
  45. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/pos_invoice.py +0 -0
  46. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/purchase_invoice.py +0 -0
  47. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/revaluation.py +0 -0
  48. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/sales_invoice.py +0 -0
  49. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/__init__.py +0 -0
  50. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/engine.py +0 -0
  51. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/pack.py +0 -0
  52. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
  53. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/packs/ch.py +0 -0
  54. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
  55. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
  56. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
  57. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/packs/generic.py +0 -0
  58. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/profiles.py +0 -0
  59. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/spine.py +0 -0
  60. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/accounting/subscription.py +0 -0
  61. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/buying/__init__.py +0 -0
  62. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/buying/purchase_order.py +0 -0
  63. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/controllers/__init__.py +0 -0
  64. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/controllers/currency.py +0 -0
  65. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/controllers/defaults.py +0 -0
  66. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/controllers/pricing_rule.py +0 -0
  67. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  68. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/database.py +0 -0
  69. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/exceptions.py +0 -0
  70. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/hooks.py +0 -0
  71. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/model.py +0 -0
  72. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/selling/__init__.py +0 -0
  73. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/selling/proposal.py +0 -0
  74. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/selling/quotation.py +0 -0
  75. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/selling/sales_order.py +0 -0
  76. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/simulation.py +0 -0
  77. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/stock/__init__.py +0 -0
  78. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/stock/delivery_note.py +0 -0
  79. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/stock/purchase_receipt.py +0 -0
  80. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/stock/stock_entry.py +0 -0
  81. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/stock/stock_ledger.py +0 -0
  82. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/lambda_erp/utils.py +0 -0
  83. {lambda_erp-0.3.3 → lambda_erp-0.4.0}/terraform/README.md +0 -0
@@ -1,3 +1,4 @@
1
+ Copyright 2026 TORUS INVESTMENTS AG
1
2
 
2
3
  Apache License
3
4
  Version 2.0, January 2004
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-erp
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: Apache-2.0
@@ -159,6 +159,7 @@ The result is booked in one step — chart of accounts, sensible default account
159
159
  - Custom analytics drafts via chat (persisted, shareable, editable)
160
160
  - Server-side aggregation tool for in-chat factual answers across large datasets
161
161
  - PDF / image attachment → add invoices, create quotations, etc. all directly by adding them in the chat
162
+ - **Programmatic API** — drive the ERP from external systems with per-user, role-capped Bearer keys: a [chat API](docs/chat-api.md) (converse with the agent) and a [REST API](docs/rest-api.md) (documents, masters, reports — the same endpoints the web app uses, so a connector or sync job can create and submit documents directly). Both off by default; an admin enables each independently and issues keys.
162
163
  - Auth with admin/manager/viewer roles plus a public demo mode
163
164
  - Full test suite that exercises every cycle against an in-memory SQLite
164
165
 
@@ -132,6 +132,7 @@ The result is booked in one step — chart of accounts, sensible default account
132
132
  - Custom analytics drafts via chat (persisted, shareable, editable)
133
133
  - Server-side aggregation tool for in-chat factual answers across large datasets
134
134
  - PDF / image attachment → add invoices, create quotations, etc. all directly by adding them in the chat
135
+ - **Programmatic API** — drive the ERP from external systems with per-user, role-capped Bearer keys: a [chat API](docs/chat-api.md) (converse with the agent) and a [REST API](docs/rest-api.md) (documents, masters, reports — the same endpoints the web app uses, so a connector or sync job can create and submit documents directly). Both off by default; an admin enables each independently and issues keys.
135
136
  - Auth with admin/manager/viewer roles plus a public demo mode
136
137
  - Full test suite that exercises every cycle against an in-memory SQLite
137
138
 
@@ -198,20 +198,48 @@ ASSIGNABLE_ROLES = {"admin", "manager", "viewer"}
198
198
 
199
199
 
200
200
  def get_current_user(request: Request) -> dict:
201
- """FastAPI dependency: extract and validate JWT from cookie.
202
- Falls back to public_manager if one exists and no cookie is set."""
203
- token = request.cookies.get(COOKIE_NAME)
201
+ """FastAPI dependency: resolve the caller for every cookie-gated REST route.
202
+
203
+ Resolution order:
204
+ 1. Session cookie (the web app) — a valid JWT wins.
205
+ 2. `Authorization: Bearer <api-key>` — a programmatic caller, gated by the
206
+ `rest_api_enabled` Settings flag (off by default). The key acts AS its
207
+ owner with the owner's live role (capped at the key's role — see
208
+ `_lookup_api_key`), so a document it creates is attributed to the real
209
+ user and every `require_role` check behaves as if that user logged in.
210
+ This is the same key type the chat API uses; the two surfaces are
211
+ gated by independent flags.
212
+ 3. Public manager (demo mode) when no credential is presented.
213
+
214
+ A malformed or invalid Bearer credential is a *failed* auth attempt, not a
215
+ fall-through — it raises 401 rather than silently downgrading to demo
216
+ access. `_lookup_api_key` / `_bearer_token` / `_setting_enabled` are defined
217
+ lower in this module and resolved at call time (late binding)."""
218
+ db = get_db()
204
219
 
220
+ token = request.cookies.get(COOKIE_NAME)
205
221
  if token:
206
222
  user_name = decode_token(token)
207
223
  if user_name:
208
- db = get_db()
209
224
  user = db.get_value("User", user_name, ["name", "email", "full_name", "role", "enabled"])
210
225
  if user and user.get("enabled"):
211
226
  return dict(user)
212
227
 
228
+ bearer = _bearer_token(request)
229
+ if bearer is not None:
230
+ if not _setting_enabled(db, "rest_api_enabled"):
231
+ raise HTTPException(status_code=401, detail="REST API key access is disabled")
232
+ key, owner, effective_role = _lookup_api_key(db, bearer)
233
+ return {
234
+ "name": owner["name"],
235
+ "email": owner.get("email"),
236
+ "full_name": owner.get("full_name"),
237
+ "role": effective_role,
238
+ "enabled": owner.get("enabled"),
239
+ "api_key_id": key["id"],
240
+ }
241
+
213
242
  # Fall back to public manager (demo mode)
214
- db = get_db()
215
243
  pub = db.sql('SELECT name, email, full_name, role, enabled FROM "User" WHERE role = \'public_manager\' AND enabled = 1')
216
244
  if pub:
217
245
  return dict(pub[0])
@@ -586,6 +614,12 @@ DEFAULTS = {
586
614
  # active. Default off — an admin turns it on and issues keys. See
587
615
  # docs/chat-api-plan.md.
588
616
  "chat_api_enabled": "0",
617
+ # When "1", the same Bearer API keys authenticate the regular REST API
618
+ # (/api/documents, /api/masters, /api/reports, …) in addition to the session
619
+ # cookie. Default off — an admin turns it on to let connectors/scripts drive
620
+ # the ERP the way the web app does. Independent of chat_api_enabled. See
621
+ # docs/rest-api.md.
622
+ "rest_api_enabled": "0",
589
623
  }
590
624
 
591
625
 
@@ -655,27 +689,32 @@ def _role_rank(role: str) -> int:
655
689
  return ROLE_HIERARCHY.get(role, 0)
656
690
 
657
691
 
658
- def get_api_caller(request: Request) -> dict:
659
- """FastAPI dependency for the programmatic chat API.
660
-
661
- Gated by the `chat_api_enabled` Settings flag — when off the whole surface
662
- 404s (never advertised on instances that don't use it). Otherwise validates
663
- the `Authorization: Bearer <token>` header against a non-revoked Api Key.
692
+ def _bearer_token(request: Request) -> str | None:
693
+ """The token from an `Authorization: Bearer <token>` header.
664
694
 
665
- Keys are per-user credentials: the effective role is resolved LIVE as
666
- min(key's role cap, owner's current role), and a disabled or deleted owner
667
- kills every one of their keys immediately. `name` is the key's
668
- session_owner (``api:<username>``) all of one user's keys share a session
669
- space, separate from their interactive web chats.
695
+ Returns None when the header is absent or not a Bearer scheme (no auth
696
+ attempt the caller may still have a cookie). Returns "" for a bare
697
+ `Bearer ` with no token a malformed *attempt* that callers treat as a
698
+ 401, distinct from None.
670
699
  """
671
- db = get_db()
672
- if not _setting_enabled(db, "chat_api_enabled"):
673
- raise HTTPException(status_code=404, detail="Not Found")
674
-
675
700
  header = request.headers.get("authorization", "")
676
701
  if not header.lower().startswith("bearer "):
677
- raise HTTPException(status_code=401, detail="Missing or malformed Authorization header")
678
- token = header[7:].strip()
702
+ return None
703
+ return header[7:].strip()
704
+
705
+
706
+ def _lookup_api_key(db, token: str) -> tuple[dict, dict, str]:
707
+ """Validate a Bearer API token → (key, owner, effective_role).
708
+
709
+ Raises 401 on any failure (empty/unknown/revoked token, or a disabled/
710
+ deleted owner). Bumps `last_used_at`. Shared by the chat API and the REST
711
+ API so both resolve a key identically — they differ only in the Settings
712
+ flag that gates them and the caller identity they build from the result.
713
+
714
+ The effective role is resolved LIVE as min(key's role cap, owner's current
715
+ role): a key can never out-rank its owner, and lowering an owner's role (or
716
+ disabling them) instantly constrains every one of their keys.
717
+ """
679
718
  if not token:
680
719
  raise HTTPException(status_code=401, detail="Empty bearer token")
681
720
 
@@ -687,15 +726,37 @@ def get_api_caller(request: Request) -> dict:
687
726
  raise HTTPException(status_code=401, detail="Invalid API key")
688
727
  key = dict(rows[0])
689
728
 
690
- owner = db.get_value("User", key["owner"], ["name", "full_name", "role", "enabled"])
729
+ owner = db.get_value("User", key["owner"], ["name", "email", "full_name", "role", "enabled"])
691
730
  if not owner or not owner.get("enabled"):
692
731
  raise HTTPException(status_code=401, detail="Invalid API key")
732
+ owner = dict(owner)
693
733
  effective_role = (
694
734
  key["role"] if _role_rank(key["role"]) <= _role_rank(owner["role"]) else owner["role"]
695
735
  )
696
736
 
697
737
  db.sql('UPDATE "Api Key" SET last_used_at = ? WHERE id = ?', [now(), key["id"]])
698
738
  db.conn.commit()
739
+ return key, owner, effective_role
740
+
741
+
742
+ def get_api_caller(request: Request) -> dict:
743
+ """FastAPI dependency for the programmatic chat API (POST /api/v1/chat).
744
+
745
+ Gated by the `chat_api_enabled` Settings flag — when off the whole surface
746
+ 404s (never advertised on instances that don't use it). Otherwise validates
747
+ the Bearer key via `_lookup_api_key`. The caller's `name` is the key's
748
+ session_owner (``api:<username>``) — all of one user's keys share a chat
749
+ session space, separate from their interactive web chats. (The REST API, by
750
+ contrast, has the key act as the real user — see `get_current_user`.)
751
+ """
752
+ db = get_db()
753
+ if not _setting_enabled(db, "chat_api_enabled"):
754
+ raise HTTPException(status_code=404, detail="Not Found")
755
+
756
+ token = _bearer_token(request)
757
+ if token is None:
758
+ raise HTTPException(status_code=401, detail="Missing or malformed Authorization header")
759
+ key, owner, effective_role = _lookup_api_key(db, token)
699
760
 
700
761
  return {
701
762
  "name": key["session_owner"],
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.3.3"
3
+ version = "0.4.0"
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
File without changes
File without changes