lambda-erp 0.1.13__tar.gz → 0.1.15__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 (65) hide show
  1. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/PKG-INFO +1 -1
  2. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/auth.py +47 -6
  3. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/routers/admin.py +1 -1
  4. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/frontend/src/api/client.ts +9 -1
  5. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/pyproject.toml +1 -1
  6. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/.gitignore +0 -0
  7. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/LICENSE +0 -0
  8. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/README.md +0 -0
  9. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/__init__.py +0 -0
  10. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/attachments.py +0 -0
  11. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/bootstrap.py +0 -0
  12. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/chat.py +0 -0
  13. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/demo_limits.py +0 -0
  14. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/deps.py +0 -0
  15. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/errors.py +0 -0
  16. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/main.py +0 -0
  17. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/pdf.py +0 -0
  18. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/providers.py +0 -0
  19. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/routers/__init__.py +0 -0
  20. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/routers/accounting.py +0 -0
  21. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/routers/analytics.py +0 -0
  22. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/routers/bank_reconciliation.py +0 -0
  23. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/routers/documents.py +0 -0
  24. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/routers/masters.py +0 -0
  25. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/routers/reports.py +0 -0
  26. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/routers/setup.py +0 -0
  27. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/services.py +0 -0
  28. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/api/templates/document.html +0 -0
  29. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/docs/agents/README.md +0 -0
  30. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/frontend/README.md +0 -0
  31. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/__init__.py +0 -0
  32. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/accounting/__init__.py +0 -0
  33. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/accounting/bank_transaction.py +0 -0
  34. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/accounting/budget.py +0 -0
  35. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  36. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/accounting/general_ledger.py +0 -0
  37. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/accounting/journal_entry.py +0 -0
  38. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/accounting/payment_entry.py +0 -0
  39. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/accounting/pos_invoice.py +0 -0
  40. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/accounting/purchase_invoice.py +0 -0
  41. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/accounting/revaluation.py +0 -0
  42. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/accounting/sales_invoice.py +0 -0
  43. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/accounting/subscription.py +0 -0
  44. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/buying/__init__.py +0 -0
  45. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/buying/purchase_order.py +0 -0
  46. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/controllers/__init__.py +0 -0
  47. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/controllers/currency.py +0 -0
  48. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/controllers/defaults.py +0 -0
  49. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/controllers/pricing_rule.py +0 -0
  50. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  51. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/database.py +0 -0
  52. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/exceptions.py +0 -0
  53. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/hooks.py +0 -0
  54. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/model.py +0 -0
  55. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/selling/__init__.py +0 -0
  56. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/selling/quotation.py +0 -0
  57. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/selling/sales_order.py +0 -0
  58. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/simulation.py +0 -0
  59. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/stock/__init__.py +0 -0
  60. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/stock/delivery_note.py +0 -0
  61. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/stock/purchase_receipt.py +0 -0
  62. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/stock/stock_entry.py +0 -0
  63. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/stock/stock_ledger.py +0 -0
  64. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/lambda_erp/utils.py +0 -0
  65. {lambda_erp-0.1.13 → lambda_erp-0.1.15}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-erp
3
- Version: 0.1.13
3
+ Version: 0.1.15
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: MIT
@@ -244,10 +244,23 @@ def require_non_public_manager(user: dict = Depends(get_current_user)) -> dict:
244
244
 
245
245
  @router.get("/setup-status")
246
246
  def auth_setup_status():
247
- """Public: check if any users exist (for first-run detection)."""
247
+ """Public: first-run detection + whether registration is open.
248
+
249
+ - first_run: no users yet — the next registrant becomes the admin.
250
+ - public_signup: the admin has enabled open self-registration (as viewer).
251
+ - registration_open: registration without an invite is possible at all
252
+ (first_run OR public_signup) — the signup form keys off this.
253
+ """
248
254
  db = get_db()
249
255
  count = db.sql('SELECT COUNT(*) as cnt FROM "User"')[0]["cnt"]
250
- return {"has_users": count > 0, "registration_open": count == 0}
256
+ first_run = count == 0
257
+ public_signup = _setting_enabled(db, "allow_public_signup")
258
+ return {
259
+ "has_users": count > 0,
260
+ "first_run": first_run,
261
+ "public_signup": public_signup,
262
+ "registration_open": first_run or public_signup,
263
+ }
251
264
 
252
265
 
253
266
  @router.post("/register")
@@ -263,11 +276,10 @@ def register(data: RegisterRequest, request: Request, response: Response):
263
276
  user_count = db.sql('SELECT COUNT(*) as cnt FROM "User"')[0]["cnt"]
264
277
 
265
278
  if user_count == 0:
279
+ # First user bootstraps the instance as admin.
266
280
  role = "admin"
267
- else:
268
- if not data.invite_token:
269
- raise HTTPException(status_code=403, detail="Registration requires an invite")
270
-
281
+ elif data.invite_token:
282
+ # Invite-only registration: the invite is bound to an email and a role.
271
283
  rows = db.sql('SELECT token, email, role, used FROM "Invite" WHERE token = ?', [data.invite_token])
272
284
  invite = rows[0] if rows else None
273
285
  if not invite:
@@ -280,6 +292,11 @@ def register(data: RegisterRequest, request: Request, response: Response):
280
292
  role = validate_assignable_role(invite["role"])
281
293
  db.sql('UPDATE "Invite" SET used = 1 WHERE token = ?', [data.invite_token])
282
294
  db.conn.commit()
295
+ elif _setting_enabled(db, "allow_public_signup"):
296
+ # Open signup is enabled: anyone may self-register, but only as a viewer.
297
+ role = "viewer"
298
+ else:
299
+ raise HTTPException(status_code=403, detail="Registration requires an invite")
283
300
 
284
301
  user_name = f"USR-{uuid.uuid4().hex[:8]}"
285
302
  db.insert("User", {
@@ -401,6 +418,20 @@ def list_invites(user: dict = Depends(require_admin)):
401
418
  return [dict(r) for r in rows]
402
419
 
403
420
 
421
+ @router.delete("/invites/{token}")
422
+ def revoke_invite(token: str, user: dict = Depends(require_admin)):
423
+ """Revoke a pending (unused) invite so its link no longer works."""
424
+ db = get_db()
425
+ rows = db.sql('SELECT token, used FROM "Invite" WHERE token = ?', [token])
426
+ if not rows:
427
+ raise HTTPException(status_code=404, detail="Invite not found")
428
+ if rows[0]["used"]:
429
+ raise HTTPException(status_code=409, detail="Invite already used")
430
+ db.sql('DELETE FROM "Invite" WHERE token = ?', [token])
431
+ db.conn.commit()
432
+ return {"ok": True}
433
+
434
+
404
435
  @router.post("/public-manager")
405
436
  def create_public_manager(user: dict = Depends(require_admin)):
406
437
  """Create (or re-enable) the public manager account for demo mode.
@@ -477,9 +508,19 @@ def get_public_manager_status():
477
508
  DEFAULTS = {
478
509
  "pdf_page_size": "A4",
479
510
  "opening_balances_enabled": "1",
511
+ # When "1", anyone may self-register (as viewer) without an invite. Default
512
+ # off: after the first user (admin), registration is invite-only.
513
+ "allow_public_signup": "0",
480
514
  }
481
515
 
482
516
 
517
+ def _setting_enabled(db, key: str) -> bool:
518
+ """Read a boolean Settings flag, honoring DEFAULTS for unset keys."""
519
+ rows = db.sql('SELECT value FROM "Settings" WHERE key = ?', [key])
520
+ value = rows[0]["value"] if rows else DEFAULTS.get(key)
521
+ return str(value) == "1"
522
+
523
+
483
524
  @router.get("/settings")
484
525
  def get_settings(user: dict = Depends(get_current_user)):
485
526
  db = get_db()
@@ -36,7 +36,7 @@ def _window_totals(now: float, seconds: int) -> dict:
36
36
  totals = db.conn.execute(
37
37
  'SELECT '
38
38
  ' COALESCE(SUM(cost_usd), 0) AS total_usd, '
39
- ' COALESCE(SUM(CASE WHEN role = "public_manager" THEN cost_usd ELSE 0 END), 0) AS demo_usd, '
39
+ ' COALESCE(SUM(CASE WHEN role = \'public_manager\' THEN cost_usd ELSE 0 END), 0) AS demo_usd, '
40
40
  ' COALESCE(SUM(prompt_tokens), 0) AS prompt_tokens, '
41
41
  ' COALESCE(SUM(completion_tokens), 0) AS completion_tokens, '
42
42
  ' COUNT(*) AS call_count, '
@@ -353,7 +353,13 @@ export const api = {
353
353
  request<any>("/setup/opening-balances/invoices", { method: "POST", body: JSON.stringify(data) }),
354
354
 
355
355
  // Auth
356
- authSetupStatus: () => request<{ has_users: boolean; registration_open: boolean }>("/auth/setup-status"),
356
+ authSetupStatus: () =>
357
+ request<{
358
+ has_users: boolean;
359
+ first_run: boolean;
360
+ public_signup: boolean;
361
+ registration_open: boolean;
362
+ }>("/auth/setup-status"),
357
363
  authRegister: (data: { email: string; full_name: string; password: string; invite_token?: string }) =>
358
364
  request<any>("/auth/register", { method: "POST", body: JSON.stringify(data) }),
359
365
  authLogin: (email: string, password: string) =>
@@ -368,6 +374,8 @@ export const api = {
368
374
  authDisableUser: (userName: string) =>
369
375
  request<any>(`/auth/users/${encodeURIComponent(userName)}`, { method: "DELETE" }),
370
376
  authListInvites: () => request<any[]>("/auth/invites"),
377
+ authRevokeInvite: (token: string) =>
378
+ request<any>(`/auth/invites/${encodeURIComponent(token)}`, { method: "DELETE" }),
371
379
  getPublicManagerStatus: () => request<{ active: boolean; user?: any }>("/auth/public-manager"),
372
380
  createPublicManager: () => request<any>("/auth/public-manager", { method: "POST" }),
373
381
  removePublicManager: () => request<any>("/auth/public-manager", { method: "DELETE" }),
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.1.13"
3
+ version = "0.1.15"
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