lambda-erp 0.3.4__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.
- {lambda_erp-0.3.4/frontend → lambda_erp-0.4.0}/LICENSE +1 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/PKG-INFO +2 -1
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/README.md +1 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/auth.py +84 -23
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/pyproject.toml +1 -1
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/.gitignore +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/__init__.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/attachments.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/bootstrap.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/chat.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/demo_limits.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/deps.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/errors.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/main.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/oauth.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/pdf.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/providers.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/remarks_md.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/routers/__init__.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/routers/accounting.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/routers/admin.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/routers/analytics.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/routers/documents.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/routers/masters.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/routers/proposals.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/routers/reports.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/routers/setup.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/services.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/templates/document.html +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/api/templates/proposal.html +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/docs/agents/README.md +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0/frontend}/LICENSE +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/frontend/README.md +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/frontend/src/api/client.ts +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/database.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/model.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.3.4 → lambda_erp-0.4.0}/terraform/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lambda-erp
|
|
3
|
-
Version: 0.
|
|
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:
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
659
|
-
"""
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
678
|
-
|
|
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"],
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|