lambda-erp 0.1.13__tar.gz → 0.1.14__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.1.13 → lambda_erp-0.1.14}/PKG-INFO +1 -1
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/auth.py +47 -6
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/routers/admin.py +1 -1
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/frontend/src/api/client.ts +9 -1
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/pyproject.toml +1 -1
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/.gitignore +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/LICENSE +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/README.md +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/__init__.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/attachments.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/bootstrap.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/chat.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/demo_limits.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/deps.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/errors.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/main.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/pdf.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/providers.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/routers/__init__.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/routers/accounting.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/routers/analytics.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/routers/documents.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/routers/masters.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/routers/reports.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/routers/setup.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/services.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/api/templates/document.html +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/docs/agents/README.md +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/frontend/README.md +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/database.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/model.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.1.13 → lambda_erp-0.1.14}/terraform/README.md +0 -0
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
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 =
|
|
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: () =>
|
|
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" }),
|
|
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
|