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.
Files changed (65) hide show
  1. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/PKG-INFO +1 -1
  2. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/auth.py +5 -5
  3. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/bootstrap.py +1 -1
  4. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/chat.py +6 -6
  5. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/main.py +1 -1
  6. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/pdf.py +1 -1
  7. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/masters.py +4 -4
  8. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/database.py +36 -14
  9. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/pyproject.toml +1 -1
  10. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/.gitignore +0 -0
  11. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/LICENSE +0 -0
  12. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/README.md +0 -0
  13. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/__init__.py +0 -0
  14. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/attachments.py +0 -0
  15. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/demo_limits.py +0 -0
  16. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/deps.py +0 -0
  17. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/errors.py +0 -0
  18. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/providers.py +0 -0
  19. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/__init__.py +0 -0
  20. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/accounting.py +0 -0
  21. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/admin.py +0 -0
  22. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/analytics.py +0 -0
  23. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/bank_reconciliation.py +0 -0
  24. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/documents.py +0 -0
  25. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/reports.py +0 -0
  26. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/routers/setup.py +0 -0
  27. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/services.py +0 -0
  28. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/api/templates/document.html +0 -0
  29. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/docs/agents/README.md +0 -0
  30. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/frontend/README.md +0 -0
  31. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/frontend/src/api/client.ts +0 -0
  32. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/__init__.py +0 -0
  33. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/__init__.py +0 -0
  34. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/bank_transaction.py +0 -0
  35. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/budget.py +0 -0
  36. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  37. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/general_ledger.py +0 -0
  38. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/journal_entry.py +0 -0
  39. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/payment_entry.py +0 -0
  40. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/pos_invoice.py +0 -0
  41. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/purchase_invoice.py +0 -0
  42. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/revaluation.py +0 -0
  43. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/sales_invoice.py +0 -0
  44. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/accounting/subscription.py +0 -0
  45. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/buying/__init__.py +0 -0
  46. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/buying/purchase_order.py +0 -0
  47. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/controllers/__init__.py +0 -0
  48. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/controllers/currency.py +0 -0
  49. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/controllers/defaults.py +0 -0
  50. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/controllers/pricing_rule.py +0 -0
  51. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  52. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/exceptions.py +0 -0
  53. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/hooks.py +0 -0
  54. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/model.py +0 -0
  55. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/selling/__init__.py +0 -0
  56. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/selling/quotation.py +0 -0
  57. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/selling/sales_order.py +0 -0
  58. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/simulation.py +0 -0
  59. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/stock/__init__.py +0 -0
  60. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/stock/delivery_note.py +0 -0
  61. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/stock/purchase_receipt.py +0 -0
  62. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/stock/stock_entry.py +0 -0
  63. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/stock/stock_ledger.py +0 -0
  64. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/lambda_erp/utils.py +0 -0
  65. {lambda_erp-0.1.5 → lambda_erp-0.1.7}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-erp
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: MIT
@@ -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 = "public_manager" AND enabled = 1')
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 = "admin" AND enabled = 1')[0]["cnt"]
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 = "public_manager"')
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 = "public_manager"')
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 = "public_manager" AND enabled = 1')
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 = "public_manager" LIMIT 1'
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 = "assistant" AND message_type = "chat"',
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 = "demo" ORDER BY id',
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 ("user", "assistant") {session_clause} {date_clause} '
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 ("user", "assistant") {session_clause} '
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 != "demo"',
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 = "demo"',
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 = "public_manager" AND enabled = 1')
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 = "pdf_page_size"')
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 = "Customer" AND party = ? LIMIT 1', "payment entry"),
71
- ('SELECT 1 FROM "Subscription" WHERE party_type = "Customer" AND party = ? LIMIT 1', "subscription"),
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 = "Supplier" AND party = ? LIMIT 1', "payment entry"),
78
- ('SELECT 1 FROM "Subscription" WHERE party_type = "Supplier" AND party = ? LIMIT 1', "subscription"),
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
- cur = self._raw.cursor()
109
- if params:
110
- # psycopg does %-substitution when params are given, so a literal %
111
- # must be doubled. No params -> psycopg leaves the SQL untouched, so
112
- # literal % is already safe and needs no escaping.
113
- cur.execute(sql.replace("%", "%%").replace("?", "%s"), list(params))
114
- else:
115
- cur.execute(sql.replace("?", "%s"))
116
- return cur
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
- cur = self._raw.cursor()
120
- cur.executemany(sql.replace("%", "%%").replace("?", "%s"),
121
- [list(p) for p in seq_params])
122
- return cur
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
- rows = cursor.fetchall()
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]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.1.5"
3
+ version = "0.1.7"
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