lambda-erp 0.1.46__tar.gz → 0.1.48__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 (71) hide show
  1. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/PKG-INFO +1 -1
  2. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/auth.py +100 -26
  3. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/chat.py +1 -1
  4. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/frontend/src/api/client.ts +6 -3
  5. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/database.py +36 -1
  6. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/pyproject.toml +1 -1
  7. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/.gitignore +0 -0
  8. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/LICENSE +0 -0
  9. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/README.md +0 -0
  10. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/__init__.py +0 -0
  11. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/attachments.py +0 -0
  12. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/bootstrap.py +0 -0
  13. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/demo_limits.py +0 -0
  14. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/deps.py +0 -0
  15. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/errors.py +0 -0
  16. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/main.py +0 -0
  17. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/oauth.py +0 -0
  18. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/pdf.py +0 -0
  19. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/providers.py +0 -0
  20. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/remarks_md.py +0 -0
  21. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/routers/__init__.py +0 -0
  22. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/routers/accounting.py +0 -0
  23. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/routers/admin.py +0 -0
  24. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/routers/analytics.py +0 -0
  25. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/routers/bank_reconciliation.py +0 -0
  26. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/routers/chat_api.py +0 -0
  27. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/routers/documents.py +0 -0
  28. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/routers/masters.py +0 -0
  29. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/routers/proposals.py +0 -0
  30. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/routers/reports.py +0 -0
  31. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/routers/setup.py +0 -0
  32. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/services.py +0 -0
  33. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/templates/document.html +0 -0
  34. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/api/templates/proposal.html +0 -0
  35. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/docs/agents/README.md +0 -0
  36. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/frontend/README.md +0 -0
  37. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/__init__.py +0 -0
  38. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/accounting/__init__.py +0 -0
  39. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/accounting/bank_transaction.py +0 -0
  40. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/accounting/budget.py +0 -0
  41. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  42. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/accounting/general_ledger.py +0 -0
  43. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/accounting/journal_entry.py +0 -0
  44. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/accounting/payment_entry.py +0 -0
  45. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/accounting/pos_invoice.py +0 -0
  46. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/accounting/purchase_invoice.py +0 -0
  47. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/accounting/revaluation.py +0 -0
  48. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/accounting/sales_invoice.py +0 -0
  49. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/accounting/subscription.py +0 -0
  50. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/buying/__init__.py +0 -0
  51. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/buying/purchase_order.py +0 -0
  52. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/controllers/__init__.py +0 -0
  53. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/controllers/currency.py +0 -0
  54. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/controllers/defaults.py +0 -0
  55. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/controllers/pricing_rule.py +0 -0
  56. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  57. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/exceptions.py +0 -0
  58. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/hooks.py +0 -0
  59. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/model.py +0 -0
  60. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/selling/__init__.py +0 -0
  61. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/selling/proposal.py +0 -0
  62. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/selling/quotation.py +0 -0
  63. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/selling/sales_order.py +0 -0
  64. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/simulation.py +0 -0
  65. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/stock/__init__.py +0 -0
  66. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/stock/delivery_note.py +0 -0
  67. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/stock/purchase_receipt.py +0 -0
  68. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/stock/stock_entry.py +0 -0
  69. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/stock/stock_ledger.py +0 -0
  70. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/lambda_erp/utils.py +0 -0
  71. {lambda_erp-0.1.46 → lambda_erp-0.1.48}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-erp
3
- Version: 0.1.46
3
+ Version: 0.1.48
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: MIT
@@ -651,14 +651,22 @@ def generate_api_key() -> tuple[str, str, str]:
651
651
  return token, token[: len(API_KEY_PREFIX) + 4], hash_api_key(token)
652
652
 
653
653
 
654
+ def _role_rank(role: str) -> int:
655
+ return ROLE_HIERARCHY.get(role, 0)
656
+
657
+
654
658
  def get_api_caller(request: Request) -> dict:
655
659
  """FastAPI dependency for the programmatic chat API.
656
660
 
657
661
  Gated by the `chat_api_enabled` Settings flag — when off the whole surface
658
662
  404s (never advertised on instances that don't use it). Otherwise validates
659
- the `Authorization: Bearer <token>` header against a non-revoked Api Key and
660
- returns a `user_info`-shaped dict the chat agent already consumes (role gates
661
- it; `name` == the key's session_owner so it can access its own sessions).
663
+ the `Authorization: Bearer <token>` header against a non-revoked Api Key.
664
+
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.
662
670
  """
663
671
  db = get_db()
664
672
  if not _setting_enabled(db, "chat_api_enabled"):
@@ -672,74 +680,123 @@ def get_api_caller(request: Request) -> dict:
672
680
  raise HTTPException(status_code=401, detail="Empty bearer token")
673
681
 
674
682
  rows = db.sql(
675
- 'SELECT id, name, role, session_owner FROM "Api Key" WHERE key_hash = ? AND revoked = 0',
683
+ 'SELECT id, name, user, role, session_owner FROM "Api Key" WHERE key_hash = ? AND revoked = 0',
676
684
  [hash_api_key(token)],
677
685
  )
678
686
  if not rows:
679
687
  raise HTTPException(status_code=401, detail="Invalid API key")
680
688
  key = dict(rows[0])
681
689
 
690
+ owner = db.get_value("User", key["user"], ["name", "full_name", "role", "enabled"])
691
+ if not owner or not owner.get("enabled"):
692
+ raise HTTPException(status_code=401, detail="Invalid API key")
693
+ effective_role = (
694
+ key["role"] if _role_rank(key["role"]) <= _role_rank(owner["role"]) else owner["role"]
695
+ )
696
+
682
697
  db.sql('UPDATE "Api Key" SET last_used_at = ? WHERE id = ?', [now(), key["id"]])
683
698
  db.conn.commit()
684
699
 
685
700
  return {
686
701
  "name": key["session_owner"],
687
- "full_name": key.get("name") or "API",
688
- "role": key["role"],
702
+ "full_name": f"{owner.get('full_name') or key['user']} (API)",
703
+ "role": effective_role,
689
704
  "api_key_id": key["id"],
705
+ "api_key_user": key["user"],
690
706
  }
691
707
 
692
708
 
693
709
  class ApiKeyCreate(BaseModel):
694
710
  name: str
695
- role: str = "manager"
711
+ # Optional role CAP. Defaults to the creator's own role; may only be equal
712
+ # or lower — a key can never out-rank its owner.
713
+ role: str | None = None
696
714
 
697
715
 
698
716
  def _serialize_api_key(row: dict) -> dict:
699
717
  return {
700
718
  "id": row["id"],
701
719
  "name": row["name"],
720
+ "user": row.get("user"),
702
721
  "role": row["role"],
703
722
  "key_prefix": row["key_prefix"],
704
- "created_by": row.get("created_by"),
705
723
  "created_at": row.get("created_at"),
706
724
  "last_used_at": row.get("last_used_at"),
707
725
  "revoked": bool(row.get("revoked")),
708
726
  }
709
727
 
710
728
 
729
+ def _get_key_or_404(db, key_id: str) -> dict:
730
+ rows = db.sql('SELECT id, user, revoked FROM "Api Key" WHERE id = ?', [key_id])
731
+ if not rows:
732
+ raise HTTPException(status_code=404, detail="API key not found")
733
+ return dict(rows[0])
734
+
735
+
736
+ def _require_key_access(key: dict, user: dict) -> None:
737
+ """A key is managed by its owner; admins can manage every key."""
738
+ if key["user"] != user["name"] and user["role"] != "admin":
739
+ raise HTTPException(status_code=403, detail="Not your API key")
740
+
741
+
711
742
  @router.get("/api-keys")
712
- def list_api_keys(user: dict = Depends(require_admin)):
713
- """List API keys (metadata only — the token is never returned)."""
743
+ def list_api_keys(user: dict = Depends(require_viewer)):
744
+ """List API keys (metadata only — the token is never returned).
745
+
746
+ Users see their own keys; admins see everyone's.
747
+ """
714
748
  db = get_db()
715
- rows = db.sql(
716
- 'SELECT id, name, role, key_prefix, created_by, created_at, last_used_at, revoked '
717
- 'FROM "Api Key" ORDER BY created_at DESC'
718
- )
749
+ if user["role"] == "admin":
750
+ rows = db.sql(
751
+ 'SELECT id, name, user, role, key_prefix, created_at, last_used_at, revoked '
752
+ 'FROM "Api Key" ORDER BY created_at DESC'
753
+ )
754
+ else:
755
+ rows = db.sql(
756
+ 'SELECT id, name, user, role, key_prefix, created_at, last_used_at, revoked '
757
+ 'FROM "Api Key" WHERE user = ? ORDER BY created_at DESC',
758
+ [user["name"]],
759
+ )
719
760
  return [_serialize_api_key(dict(r)) for r in rows]
720
761
 
721
762
 
722
763
  @router.post("/api-keys")
723
- def create_api_key(data: ApiKeyCreate, user: dict = Depends(require_admin)):
724
- """Create an API key. Returns the full token exactly once."""
764
+ def create_api_key(data: ApiKeyCreate, user: dict = Depends(require_viewer)):
765
+ """Create an API key for YOURSELF. Returns the full token exactly once.
766
+
767
+ Self-service: any logged-in (non-demo) user can mint keys, because a key
768
+ can never exceed its owner's live role — the optional `role` is a cap that
769
+ must be equal or lower.
770
+ """
771
+ if user["role"] == "public_manager":
772
+ raise HTTPException(status_code=403, detail="Demo mode cannot create API keys.")
725
773
  name = (data.name or "").strip()
726
774
  if not name:
727
775
  raise HTTPException(status_code=422, detail="A name is required.")
728
- role = data.role if data.role in API_KEY_ROLES else "manager"
776
+
777
+ role = data.role or user["role"]
778
+ if role not in API_KEY_ROLES:
779
+ raise HTTPException(status_code=422, detail=f"Invalid role: {role}")
780
+ if _role_rank(role) > _role_rank(user["role"]):
781
+ raise HTTPException(
782
+ status_code=403,
783
+ detail=f"A key cannot out-rank its owner (your role: {user['role']}).",
784
+ )
729
785
 
730
786
  db = get_db()
731
787
  key_id = str(uuid.uuid4())
732
788
  token, key_prefix, key_hash = generate_api_key()
789
+ created_at = now()
733
790
  db.sql(
734
791
  'INSERT INTO "Api Key" '
735
- '(id, name, key_hash, key_prefix, role, session_owner, created_by, created_at, last_used_at, revoked) '
792
+ '(id, name, user, key_hash, key_prefix, role, session_owner, created_at, last_used_at, revoked) '
736
793
  'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)',
737
- [key_id, name, key_hash, key_prefix, role, f"apikey:{key_id}", user["name"], now(), None],
794
+ [key_id, name, user["name"], key_hash, key_prefix, role, f"api:{user['name']}", created_at, None],
738
795
  )
739
796
  db.conn.commit()
740
797
  row = {
741
- "id": key_id, "name": name, "role": role, "key_prefix": key_prefix,
742
- "created_by": user["name"], "created_at": now(), "last_used_at": None, "revoked": 0,
798
+ "id": key_id, "name": name, "user": user["name"], "role": role, "key_prefix": key_prefix,
799
+ "created_at": created_at, "last_used_at": None, "revoked": 0,
743
800
  }
744
801
  result = _serialize_api_key(row)
745
802
  result["token"] = token # shown once
@@ -747,12 +804,29 @@ def create_api_key(data: ApiKeyCreate, user: dict = Depends(require_admin)):
747
804
 
748
805
 
749
806
  @router.post("/api-keys/{key_id}/revoke")
750
- def revoke_api_key(key_id: str, user: dict = Depends(require_admin)):
751
- """Revoke (soft-disable) an API key without deleting its history."""
807
+ def revoke_api_key(key_id: str, user: dict = Depends(require_viewer)):
808
+ """Revoke (soft-disable) an API key. Owner or admin."""
752
809
  db = get_db()
753
- rows = db.sql('SELECT id FROM "Api Key" WHERE id = ?', [key_id])
754
- if not rows:
755
- raise HTTPException(status_code=404, detail="API key not found")
810
+ key = _get_key_or_404(db, key_id)
811
+ _require_key_access(key, user)
756
812
  db.sql('UPDATE "Api Key" SET revoked = 1 WHERE id = ?', [key_id])
757
813
  db.conn.commit()
758
814
  return {"id": key_id, "revoked": True}
815
+
816
+
817
+ @router.delete("/api-keys/{key_id}")
818
+ def delete_api_key(key_id: str, user: dict = Depends(require_viewer)):
819
+ """Hard-delete a REVOKED API key. Owner or admin.
820
+
821
+ Two-step by design: a live key must be revoked first (409 otherwise), so a
822
+ key can't vanish while still usable. Past sessions keep their session_owner
823
+ string, so historical attribution survives the row's deletion.
824
+ """
825
+ db = get_db()
826
+ key = _get_key_or_404(db, key_id)
827
+ _require_key_access(key, user)
828
+ if not key.get("revoked"):
829
+ raise HTTPException(status_code=409, detail="Revoke the key first, then delete it.")
830
+ db.sql('DELETE FROM "Api Key" WHERE id = ?', [key_id])
831
+ db.conn.commit()
832
+ return {"id": key_id, "deleted": True}
@@ -1913,7 +1913,7 @@ For purchased goods, prefer Purchase Receipt when receiving separately from bill
1913
1913
  customer, supplier, item, warehouse, account, company, cost-center
1914
1914
 
1915
1915
  ## Master Data Deletion
1916
- Master records CAN be deleted — use the `delete_master` tool (ADMIN role only). It is reference-protected: an unreferenced record (e.g. a mistakenly created or duplicate one) is permanently deleted; a record referenced by any document, GL entry, or stock data is NOT deleted but automatically disabled/archived instead, and the result names the blocking reference — relay that reason to the user. When the user merely wants a record out of the way (not destroyed), prefer `update_master` with `{"disabled": 1}`. Non-admin users: explain that deletion needs an admin and offer to disable instead.
1916
+ Master records CAN be deleted — use the `delete_master` tool (ADMIN role only). It is reference-protected: an unreferenced record (e.g. a mistakenly created or duplicate one) is permanently deleted; a record referenced by any document, GL entry, or stock data is NOT deleted but automatically disabled/archived instead, and the result names the blocking reference — relay that reason to the user. When the user merely wants a record out of the way (not destroyed), prefer `update_master` with `{{"disabled": 1}}`. Non-admin users: explain that deletion needs an admin and offer to disable instead.
1917
1917
 
1918
1918
  ## Warehouse Master Rules
1919
1919
  - To create a warehouse via `create_master`, you MUST pass a `data` object.
@@ -452,15 +452,18 @@ export const api = {
452
452
  request<Record<string, string>>("/auth/settings", { method: "PUT", body: JSON.stringify(data) }),
453
453
  getApiKeys: () =>
454
454
  request<Array<{
455
- id: string; name: string; role: string; key_prefix: string;
456
- created_by?: string; created_at?: string; last_used_at?: string | null; revoked: boolean;
455
+ id: string; name: string; user?: string; role: string; key_prefix: string;
456
+ created_at?: string; last_used_at?: string | null; revoked: boolean;
457
457
  }>>("/auth/api-keys"),
458
458
  createApiKey: (name: string, role: string) =>
459
- request<{ id: string; name: string; role: string; key_prefix: string; token: string }>(
459
+ request<{ id: string; name: string; user?: string; role: string; key_prefix: string; token: string }>(
460
460
  "/auth/api-keys", { method: "POST", body: JSON.stringify({ name, role }) }),
461
461
  revokeApiKey: (id: string) =>
462
462
  request<{ id: string; revoked: boolean }>(
463
463
  `/auth/api-keys/${encodeURIComponent(id)}/revoke`, { method: "POST" }),
464
+ deleteApiKey: (id: string) =>
465
+ request<{ id: string; deleted: boolean }>(
466
+ `/auth/api-keys/${encodeURIComponent(id)}`, { method: "DELETE" }),
464
467
 
465
468
  getDemoSpend: () =>
466
469
  request<{
@@ -1314,14 +1314,18 @@ class Database:
1314
1314
  # agent's tool access; `session_owner` is the user_id the key's chat
1315
1315
  # sessions are owned under (isolated from human users). See
1316
1316
  # docs/chat-api-plan.md.
1317
+ # Per-user API keys (v2): a key BELONGS to a User and can never act
1318
+ # above its owner — `role` is only a CAP; the effective role is
1319
+ # min(cap, owner's current role) resolved live at auth time, so a
1320
+ # demotion or disable applies to existing keys immediately.
1317
1321
  """CREATE TABLE IF NOT EXISTS "Api Key" (
1318
1322
  id TEXT PRIMARY KEY,
1319
1323
  name TEXT,
1324
+ user TEXT NOT NULL,
1320
1325
  key_hash TEXT UNIQUE,
1321
1326
  key_prefix TEXT,
1322
1327
  role TEXT DEFAULT 'manager',
1323
1328
  session_owner TEXT,
1324
- created_by TEXT,
1325
1329
  created_at TEXT,
1326
1330
  last_used_at TEXT,
1327
1331
  revoked INTEGER DEFAULT 0
@@ -1894,6 +1898,36 @@ def _m018_quotation_item_frequency(db: "Database") -> None:
1894
1898
  db._add_column_if_missing("Quotation Item", "frequency", "TEXT")
1895
1899
 
1896
1900
 
1901
+ def _m019_api_keys_per_user(db: "Database") -> None:
1902
+ """API keys v2 — per-user credentials (clean break, deliberate).
1903
+
1904
+ v1 keys were org-global objects minted by admins with a free-standing role
1905
+ and a synthetic identity; anyone holding the token WAS the key. v2 binds
1906
+ every key to a User (self-service creation, effective role capped by the
1907
+ owner's live role). Existing v1 rows are DROPPED, not migrated — decided
1908
+ 2026-07-14: no legacy rules; holders simply re-issue their keys.
1909
+ """
1910
+ if "user" in db._get_table_columns("Api Key"):
1911
+ return # fresh install — table already has the v2 shape
1912
+ db.conn.execute('DROP TABLE "Api Key"')
1913
+ db.conn.execute(
1914
+ """CREATE TABLE "Api Key" (
1915
+ id TEXT PRIMARY KEY,
1916
+ name TEXT,
1917
+ user TEXT NOT NULL,
1918
+ key_hash TEXT UNIQUE,
1919
+ key_prefix TEXT,
1920
+ role TEXT DEFAULT 'manager',
1921
+ session_owner TEXT,
1922
+ created_at TEXT,
1923
+ last_used_at TEXT,
1924
+ revoked INTEGER DEFAULT 0
1925
+ )"""
1926
+ )
1927
+ db._col_cache.pop("Api Key", None)
1928
+ db._text_col_cache.pop("Api Key", None)
1929
+
1930
+
1897
1931
  Database.MIGRATIONS = [
1898
1932
  (1, "chat_message_session_id", _m001_chat_message_session_id),
1899
1933
  (2, "chat_session_user_id", _m002_chat_session_user_id),
@@ -1913,6 +1947,7 @@ Database.MIGRATIONS = [
1913
1947
  (16, "customer_contact_person", _m016_customer_contact_person),
1914
1948
  (17, "proposal_cover_template", _m017_proposal_cover_template),
1915
1949
  (18, "quotation_item_frequency", _m018_quotation_item_frequency),
1950
+ (19, "api_keys_per_user", _m019_api_keys_per_user),
1916
1951
  ]
1917
1952
 
1918
1953
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.1.46"
3
+ version = "0.1.48"
4
4
  description = "Core ERP logic - accounting, sales, purchasing, inventory"
5
5
  readme = "README.md"
6
6
  license = "MIT"
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