lambda-erp 0.1.12__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.12 → lambda_erp-0.1.14}/PKG-INFO +1 -1
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/auth.py +47 -6
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/pdf.py +30 -2
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/routers/admin.py +1 -1
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/frontend/src/api/client.ts +9 -1
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/database.py +7 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/pyproject.toml +1 -1
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/.gitignore +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/LICENSE +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/README.md +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/__init__.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/attachments.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/bootstrap.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/chat.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/demo_limits.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/deps.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/errors.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/main.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/providers.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/routers/__init__.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/routers/accounting.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/routers/analytics.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/routers/documents.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/routers/masters.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/routers/reports.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/routers/setup.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/services.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/api/templates/document.html +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/docs/agents/README.md +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/frontend/README.md +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/model.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.1.12 → lambda_erp-0.1.14}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.1.12 → 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()
|
|
@@ -37,6 +37,22 @@ def register_pdf_template_dir(path: str) -> None:
|
|
|
37
37
|
_plugin_template_dirs.insert(0, path)
|
|
38
38
|
_jinja_env.loader = _build_loader()
|
|
39
39
|
|
|
40
|
+
|
|
41
|
+
_pdf_context_providers = []
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def register_pdf_context(fn) -> None:
|
|
45
|
+
"""Register a provider that augments the PDF render context.
|
|
46
|
+
|
|
47
|
+
`fn(doctype, name, context)` is called just before rendering with the
|
|
48
|
+
assembled context (doc, company_info, party_info, currency, items, …) and
|
|
49
|
+
may return a dict of EXTRA keys to merge in — e.g. a computed Swiss QR-bill
|
|
50
|
+
image for invoices. Lets a deployment add per-document content the template
|
|
51
|
+
can't compute itself. Exceptions are swallowed so a buggy provider can't
|
|
52
|
+
break PDF generation.
|
|
53
|
+
"""
|
|
54
|
+
_pdf_context_providers.append(fn)
|
|
55
|
+
|
|
40
56
|
# Document type → (title, party_field, party_name_field, party_doctype, party_label)
|
|
41
57
|
DOC_CONFIG = {
|
|
42
58
|
"Quotation": ("Quotation", "customer", "customer_name", "Customer", "Customer"),
|
|
@@ -114,7 +130,7 @@ def generate_pdf(doctype_slug: str, name: str) -> bytes:
|
|
|
114
130
|
company_info = {}
|
|
115
131
|
if company_id:
|
|
116
132
|
row = db.get_value("Company", company_id,
|
|
117
|
-
["company_name", "email", "phone", "address", "city", "zip_code", "country", "tax_id"])
|
|
133
|
+
["company_name", "email", "phone", "address", "city", "zip_code", "country", "tax_id", "iban"])
|
|
118
134
|
if row:
|
|
119
135
|
company_info = _get_dict(row)
|
|
120
136
|
# Show the company's CURRENT display name from the master.
|
|
@@ -164,7 +180,7 @@ def generate_pdf(doctype_slug: str, name: str) -> bytes:
|
|
|
164
180
|
# plugin's branded override) can reference sibling assets — logo.png, fonts,
|
|
165
181
|
# CSS — by relative path. Falls back to the built-in templates dir.
|
|
166
182
|
base_url = template.filename or os.path.join(TEMPLATE_DIR, "document.html")
|
|
167
|
-
|
|
183
|
+
context = dict(
|
|
168
184
|
doc=doc,
|
|
169
185
|
title=title,
|
|
170
186
|
company_name=company_name,
|
|
@@ -180,4 +196,16 @@ def generate_pdf(doctype_slug: str, name: str) -> bytes:
|
|
|
180
196
|
page_size=page_size,
|
|
181
197
|
)
|
|
182
198
|
|
|
199
|
+
# Let deployment plugins augment the context (e.g. a Swiss QR-bill image for
|
|
200
|
+
# invoices). A provider that raises must not break PDF generation.
|
|
201
|
+
for provider in _pdf_context_providers:
|
|
202
|
+
try:
|
|
203
|
+
extra = provider(doctype, name, context)
|
|
204
|
+
if extra:
|
|
205
|
+
context.update(extra)
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
html_str = template.render(**context)
|
|
210
|
+
|
|
183
211
|
return HTML(string=html_str, base_url=base_url).write_pdf()
|
|
@@ -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" }),
|
|
@@ -270,6 +270,7 @@ class Database:
|
|
|
270
270
|
zip_code TEXT,
|
|
271
271
|
country TEXT,
|
|
272
272
|
tax_id TEXT,
|
|
273
|
+
iban TEXT,
|
|
273
274
|
default_cost_center TEXT,
|
|
274
275
|
round_off_account TEXT,
|
|
275
276
|
round_off_cost_center TEXT,
|
|
@@ -1707,6 +1708,11 @@ def _m013_unrealized_exchange_account(db: "Database") -> None:
|
|
|
1707
1708
|
)
|
|
1708
1709
|
|
|
1709
1710
|
|
|
1711
|
+
def _m014_company_iban(db: "Database") -> None:
|
|
1712
|
+
"""Add Company.iban (bank account for payment instructions / QR-bills)."""
|
|
1713
|
+
db._add_column_if_missing("Company", "iban", "TEXT")
|
|
1714
|
+
|
|
1715
|
+
|
|
1710
1716
|
Database.MIGRATIONS = [
|
|
1711
1717
|
(1, "chat_message_session_id", _m001_chat_message_session_id),
|
|
1712
1718
|
(2, "chat_session_user_id", _m002_chat_session_user_id),
|
|
@@ -1721,6 +1727,7 @@ Database.MIGRATIONS = [
|
|
|
1721
1727
|
(11, "payment_entry_currency", _m011_payment_entry_currency),
|
|
1722
1728
|
(12, "exchange_gain_loss_account", _m012_exchange_gain_loss_account),
|
|
1723
1729
|
(13, "unrealized_exchange_account", _m013_unrealized_exchange_account),
|
|
1730
|
+
(14, "company_iban", _m014_company_iban),
|
|
1724
1731
|
]
|
|
1725
1732
|
|
|
1726
1733
|
|
|
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
|