lambda-erp 0.1.5__tar.gz → 0.1.7__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.5 → lambda_erp-0.1.7}/PKG-INFO +1 -1
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/auth.py +5 -5
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/bootstrap.py +1 -1
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/chat.py +6 -6
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/main.py +1 -1
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/pdf.py +1 -1
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/masters.py +4 -4
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/database.py +36 -14
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/pyproject.toml +1 -1
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/.gitignore +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/LICENSE +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/README.md +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/__init__.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/attachments.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/demo_limits.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/deps.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/errors.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/providers.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/__init__.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/accounting.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/admin.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/analytics.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/documents.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/reports.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/setup.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/services.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/templates/document.html +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/docs/agents/README.md +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/frontend/README.md +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/frontend/src/api/client.ts +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/model.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.1.5 → lambda_erp-0.1.7}/terraform/README.md +0 -0
|
@@ -196,7 +196,7 @@ def get_current_user(request: Request) -> dict:
|
|
|
196
196
|
|
|
197
197
|
# Fall back to public manager (demo mode)
|
|
198
198
|
db = get_db()
|
|
199
|
-
pub = db.sql('SELECT name, email, full_name, role, enabled FROM "User" WHERE role =
|
|
199
|
+
pub = db.sql('SELECT name, email, full_name, role, enabled FROM "User" WHERE role = \'public_manager\' AND enabled = 1')
|
|
200
200
|
if pub:
|
|
201
201
|
return dict(pub[0])
|
|
202
202
|
|
|
@@ -373,7 +373,7 @@ def change_role(user_name: str, data: ChangeRoleRequest, user: dict = Depends(re
|
|
|
373
373
|
raise HTTPException(status_code=404, detail="User not found")
|
|
374
374
|
|
|
375
375
|
if user_name == user["name"] and data.role != "admin":
|
|
376
|
-
admin_count = db.sql('SELECT COUNT(*) as cnt FROM "User" WHERE role =
|
|
376
|
+
admin_count = db.sql('SELECT COUNT(*) as cnt FROM "User" WHERE role = \'admin\' AND enabled = 1')[0]["cnt"]
|
|
377
377
|
if admin_count <= 1:
|
|
378
378
|
raise HTTPException(status_code=409, detail="Cannot demote the only admin")
|
|
379
379
|
|
|
@@ -414,7 +414,7 @@ def create_public_manager(user: dict = Depends(require_admin)):
|
|
|
414
414
|
turned on.
|
|
415
415
|
"""
|
|
416
416
|
db = get_db()
|
|
417
|
-
existing = db.sql('SELECT name, enabled FROM "User" WHERE role =
|
|
417
|
+
existing = db.sql('SELECT name, enabled FROM "User" WHERE role = \'public_manager\'')
|
|
418
418
|
if existing:
|
|
419
419
|
if not existing[0]["enabled"]:
|
|
420
420
|
db.set_value("User", existing[0]["name"], {"enabled": 1, "modified": now()})
|
|
@@ -453,7 +453,7 @@ def create_public_manager(user: dict = Depends(require_admin)):
|
|
|
453
453
|
def remove_public_manager(user: dict = Depends(require_admin)):
|
|
454
454
|
"""Disable the public manager account."""
|
|
455
455
|
db = get_db()
|
|
456
|
-
existing = db.sql('SELECT name FROM "User" WHERE role =
|
|
456
|
+
existing = db.sql('SELECT name FROM "User" WHERE role = \'public_manager\'')
|
|
457
457
|
if not existing:
|
|
458
458
|
return {"ok": True, "status": "not_found"}
|
|
459
459
|
db.set_value("User", existing[0]["name"], {"enabled": 0, "modified": now()})
|
|
@@ -464,7 +464,7 @@ def remove_public_manager(user: dict = Depends(require_admin)):
|
|
|
464
464
|
def get_public_manager_status():
|
|
465
465
|
"""Public: check if a public manager exists and is enabled."""
|
|
466
466
|
db = get_db()
|
|
467
|
-
existing = db.sql('SELECT name, full_name, role, enabled FROM "User" WHERE role =
|
|
467
|
+
existing = db.sql('SELECT name, full_name, role, enabled FROM "User" WHERE role = \'public_manager\' AND enabled = 1')
|
|
468
468
|
if existing:
|
|
469
469
|
return {"active": True, "user": dict(existing[0])}
|
|
470
470
|
return {"active": False}
|
|
@@ -477,7 +477,7 @@ def _format_qty(value) -> str:
|
|
|
477
477
|
|
|
478
478
|
def _public_manager_user(db) -> dict | None:
|
|
479
479
|
rows = db.sql(
|
|
480
|
-
'SELECT name, role FROM "User" WHERE role =
|
|
480
|
+
'SELECT name, role FROM "User" WHERE role = \'public_manager\' LIMIT 1'
|
|
481
481
|
)
|
|
482
482
|
if not rows:
|
|
483
483
|
return None
|
|
@@ -297,7 +297,7 @@ def count_assistant_messages(session_id: str) -> int:
|
|
|
297
297
|
"""Count assistant messages in a session (used to decide when to generate title)."""
|
|
298
298
|
db = get_db()
|
|
299
299
|
rows = db.sql(
|
|
300
|
-
'SELECT COUNT(*) as cnt FROM "Chat Message" WHERE session_id = ? AND role =
|
|
300
|
+
'SELECT COUNT(*) as cnt FROM "Chat Message" WHERE session_id = ? AND role = \'assistant\' AND message_type = \'chat\'',
|
|
301
301
|
[session_id],
|
|
302
302
|
)
|
|
303
303
|
return rows[0]["cnt"] if rows else 0
|
|
@@ -385,7 +385,7 @@ def load_demo_history(session_id: str) -> list[dict]:
|
|
|
385
385
|
db = get_db()
|
|
386
386
|
rows = db.sql(
|
|
387
387
|
'SELECT role, content, created_at FROM "Chat Message" '
|
|
388
|
-
'WHERE session_id = ? AND message_type =
|
|
388
|
+
'WHERE session_id = ? AND message_type = \'demo\' ORDER BY id',
|
|
389
389
|
[session_id],
|
|
390
390
|
)
|
|
391
391
|
return [dict(r) for r in rows]
|
|
@@ -1172,7 +1172,7 @@ def _handle_retrieve_chat_history(args, session_id=None):
|
|
|
1172
1172
|
params.append(date_to)
|
|
1173
1173
|
rows = db.sql(
|
|
1174
1174
|
f'SELECT role, content, created_at FROM "Chat Message" '
|
|
1175
|
-
f'WHERE role IN (
|
|
1175
|
+
f'WHERE role IN (\'user\', \'assistant\') {session_clause} {date_clause} '
|
|
1176
1176
|
f'ORDER BY id ASC LIMIT 50',
|
|
1177
1177
|
params,
|
|
1178
1178
|
)
|
|
@@ -1180,7 +1180,7 @@ def _handle_retrieve_chat_history(args, session_id=None):
|
|
|
1180
1180
|
num = min(int(args.get("num_messages", 20)), 50)
|
|
1181
1181
|
rows = db.sql(
|
|
1182
1182
|
f'SELECT role, content, created_at FROM "Chat Message" '
|
|
1183
|
-
f'WHERE role IN (
|
|
1183
|
+
f'WHERE role IN (\'user\', \'assistant\') {session_clause} '
|
|
1184
1184
|
f'ORDER BY id DESC LIMIT ?',
|
|
1185
1185
|
session_params + [num],
|
|
1186
1186
|
)
|
|
@@ -2624,7 +2624,7 @@ async def chat_websocket(
|
|
|
2624
2624
|
# (non-demo) user/assistant messages.
|
|
2625
2625
|
non_demo_rows = get_db().sql(
|
|
2626
2626
|
'SELECT COUNT(*) as cnt FROM "Chat Message" '
|
|
2627
|
-
'WHERE session_id = ? AND message_type !=
|
|
2627
|
+
'WHERE session_id = ? AND message_type != \'demo\'',
|
|
2628
2628
|
[session_id],
|
|
2629
2629
|
)
|
|
2630
2630
|
if non_demo_rows and non_demo_rows[0]["cnt"] > 0:
|
|
@@ -2643,7 +2643,7 @@ async def chat_websocket(
|
|
|
2643
2643
|
db = get_db()
|
|
2644
2644
|
db.sql(
|
|
2645
2645
|
'DELETE FROM "Chat Message" '
|
|
2646
|
-
'WHERE session_id = ? AND message_type =
|
|
2646
|
+
'WHERE session_id = ? AND message_type = \'demo\'',
|
|
2647
2647
|
[session_id],
|
|
2648
2648
|
)
|
|
2649
2649
|
db.conn.commit()
|
|
@@ -121,7 +121,7 @@ async def ws_chat(websocket: WebSocket):
|
|
|
121
121
|
|
|
122
122
|
# Fall back to public manager
|
|
123
123
|
if not user:
|
|
124
|
-
pub = db.sql('SELECT name, full_name, role, enabled FROM "User" WHERE role =
|
|
124
|
+
pub = db.sql('SELECT name, full_name, role, enabled FROM "User" WHERE role = \'public_manager\' AND enabled = 1')
|
|
125
125
|
user = pub[0] if pub else None
|
|
126
126
|
|
|
127
127
|
if not user:
|
|
@@ -127,7 +127,7 @@ def generate_pdf(doctype_slug: str, name: str) -> bytes:
|
|
|
127
127
|
taxes = doc.get("taxes", [])
|
|
128
128
|
|
|
129
129
|
# Page size setting
|
|
130
|
-
page_size_row = db.sql('SELECT value FROM "Settings" WHERE key =
|
|
130
|
+
page_size_row = db.sql('SELECT value FROM "Settings" WHERE key = \'pdf_page_size\'')
|
|
131
131
|
page_size = page_size_row[0]["value"] if page_size_row else "A4"
|
|
132
132
|
|
|
133
133
|
# Render
|
|
@@ -67,15 +67,15 @@ DELETE_REFERENCE_CHECKS = {
|
|
|
67
67
|
('SELECT 1 FROM "Sales Invoice" WHERE customer = ? LIMIT 1', "sales invoice"),
|
|
68
68
|
('SELECT 1 FROM "Delivery Note" WHERE customer = ? LIMIT 1', "delivery note"),
|
|
69
69
|
('SELECT 1 FROM "POS Invoice" WHERE customer = ? LIMIT 1', "POS invoice"),
|
|
70
|
-
('SELECT 1 FROM "Payment Entry" WHERE party_type =
|
|
71
|
-
('SELECT 1 FROM "Subscription" WHERE party_type =
|
|
70
|
+
('SELECT 1 FROM "Payment Entry" WHERE party_type = \'Customer\' AND party = ? LIMIT 1', "payment entry"),
|
|
71
|
+
('SELECT 1 FROM "Subscription" WHERE party_type = \'Customer\' AND party = ? LIMIT 1', "subscription"),
|
|
72
72
|
],
|
|
73
73
|
"supplier": [
|
|
74
74
|
('SELECT 1 FROM "Purchase Order" WHERE supplier = ? LIMIT 1', "purchase order"),
|
|
75
75
|
('SELECT 1 FROM "Purchase Invoice" WHERE supplier = ? LIMIT 1', "purchase invoice"),
|
|
76
76
|
('SELECT 1 FROM "Purchase Receipt" WHERE supplier = ? LIMIT 1', "purchase receipt"),
|
|
77
|
-
('SELECT 1 FROM "Payment Entry" WHERE party_type =
|
|
78
|
-
('SELECT 1 FROM "Subscription" WHERE party_type =
|
|
77
|
+
('SELECT 1 FROM "Payment Entry" WHERE party_type = \'Supplier\' AND party = ? LIMIT 1', "payment entry"),
|
|
78
|
+
('SELECT 1 FROM "Subscription" WHERE party_type = \'Supplier\' AND party = ? LIMIT 1', "subscription"),
|
|
79
79
|
],
|
|
80
80
|
"item": [
|
|
81
81
|
('SELECT 1 FROM "Quotation Item" WHERE item_code = ? LIMIT 1', "quotation item"),
|
|
@@ -105,21 +105,39 @@ class _PgConn:
|
|
|
105
105
|
self._raw = raw
|
|
106
106
|
|
|
107
107
|
def execute(self, sql, params=None):
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
108
|
+
try:
|
|
109
|
+
cur = self._raw.cursor()
|
|
110
|
+
if params:
|
|
111
|
+
# psycopg does %-substitution when params are given, so a literal
|
|
112
|
+
# % must be doubled. No params -> psycopg leaves the SQL
|
|
113
|
+
# untouched, so literal % is already safe and needs no escaping.
|
|
114
|
+
cur.execute(sql.replace("%", "%%").replace("?", "%s"), list(params))
|
|
115
|
+
else:
|
|
116
|
+
cur.execute(sql.replace("?", "%s"))
|
|
117
|
+
return cur
|
|
118
|
+
except Exception:
|
|
119
|
+
# A failed statement leaves the connection in an aborted transaction
|
|
120
|
+
# (autocommit=False); every later query on this pooled/thread-local
|
|
121
|
+
# connection would then fail with InFailedSqlTransaction. Roll back
|
|
122
|
+
# so the connection stays usable, then re-raise the real error.
|
|
123
|
+
self._safe_rollback()
|
|
124
|
+
raise
|
|
117
125
|
|
|
118
126
|
def executemany(self, sql, seq_params):
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
try:
|
|
128
|
+
cur = self._raw.cursor()
|
|
129
|
+
cur.executemany(sql.replace("%", "%%").replace("?", "%s"),
|
|
130
|
+
[list(p) for p in seq_params])
|
|
131
|
+
return cur
|
|
132
|
+
except Exception:
|
|
133
|
+
self._safe_rollback()
|
|
134
|
+
raise
|
|
135
|
+
|
|
136
|
+
def _safe_rollback(self):
|
|
137
|
+
try:
|
|
138
|
+
self._raw.rollback()
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
123
141
|
|
|
124
142
|
def commit(self):
|
|
125
143
|
self._raw.commit()
|
|
@@ -1308,7 +1326,11 @@ class Database:
|
|
|
1308
1326
|
"""
|
|
1309
1327
|
with self._lock:
|
|
1310
1328
|
cursor = self.conn.execute(query, values or [])
|
|
1311
|
-
|
|
1329
|
+
# Only fetch when the statement produced a result set. SQLite's
|
|
1330
|
+
# fetchall() after an INSERT/UPDATE/DELETE harmlessly returns [];
|
|
1331
|
+
# psycopg raises ("the last operation didn't produce records"). A
|
|
1332
|
+
# NULL cursor.description means no result set on both drivers.
|
|
1333
|
+
rows = cursor.fetchall() if cursor.description is not None else []
|
|
1312
1334
|
if as_dict:
|
|
1313
1335
|
return [_dict(dict(row)) for row in rows]
|
|
1314
1336
|
return [tuple(row) for row in rows]
|
|
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
|