lambda-erp 0.6.20__tar.gz → 0.6.22__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 (84) hide show
  1. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/PKG-INFO +1 -1
  2. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/errors.py +55 -0
  3. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/routers/masters.py +4 -1
  4. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/pyproject.toml +1 -1
  5. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/.gitignore +0 -0
  6. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/LICENSE +0 -0
  7. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/README.md +0 -0
  8. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/__init__.py +0 -0
  9. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/attachments.py +0 -0
  10. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/auth.py +0 -0
  11. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/bootstrap.py +0 -0
  12. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/chat.py +0 -0
  13. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/demo_limits.py +0 -0
  14. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/deps.py +0 -0
  15. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/main.py +0 -0
  16. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/oauth.py +0 -0
  17. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/pdf.py +0 -0
  18. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/providers.py +0 -0
  19. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/remarks_md.py +0 -0
  20. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/routers/__init__.py +0 -0
  21. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/routers/accounting.py +0 -0
  22. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/routers/admin.py +0 -0
  23. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/routers/analytics.py +0 -0
  24. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/routers/bank_reconciliation.py +0 -0
  25. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/routers/chat_api.py +0 -0
  26. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/routers/documents.py +0 -0
  27. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/routers/mcp.py +0 -0
  28. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/routers/proposals.py +0 -0
  29. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/routers/reports.py +0 -0
  30. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/routers/setup.py +0 -0
  31. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/services.py +0 -0
  32. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/templates/document.html +0 -0
  33. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/api/templates/proposal.html +0 -0
  34. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/docs/agents/README.md +0 -0
  35. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/frontend/LICENSE +0 -0
  36. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/frontend/README.md +0 -0
  37. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/frontend/src/api/client.ts +0 -0
  38. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/__init__.py +0 -0
  39. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/__init__.py +0 -0
  40. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/bank_transaction.py +0 -0
  41. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/budget.py +0 -0
  42. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  43. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/general_ledger.py +0 -0
  44. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/journal_entry.py +0 -0
  45. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/payment_entry.py +0 -0
  46. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/pos_invoice.py +0 -0
  47. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/purchase_invoice.py +0 -0
  48. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/revaluation.py +0 -0
  49. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/sales_invoice.py +0 -0
  50. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/setup/__init__.py +0 -0
  51. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/setup/engine.py +0 -0
  52. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/setup/pack.py +0 -0
  53. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
  54. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/setup/packs/ch.py +0 -0
  55. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
  56. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
  57. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
  58. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/setup/packs/generic.py +0 -0
  59. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/setup/profiles.py +0 -0
  60. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/setup/spine.py +0 -0
  61. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/accounting/subscription.py +0 -0
  62. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/buying/__init__.py +0 -0
  63. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/buying/purchase_order.py +0 -0
  64. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/controllers/__init__.py +0 -0
  65. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/controllers/currency.py +0 -0
  66. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/controllers/defaults.py +0 -0
  67. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/controllers/pricing_rule.py +0 -0
  68. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  69. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/database.py +0 -0
  70. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/exceptions.py +0 -0
  71. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/hooks.py +0 -0
  72. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/model.py +0 -0
  73. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/selling/__init__.py +0 -0
  74. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/selling/proposal.py +0 -0
  75. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/selling/quotation.py +0 -0
  76. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/selling/sales_order.py +0 -0
  77. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/simulation.py +0 -0
  78. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/stock/__init__.py +0 -0
  79. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/stock/delivery_note.py +0 -0
  80. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/stock/purchase_receipt.py +0 -0
  81. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/stock/stock_entry.py +0 -0
  82. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/stock/stock_ledger.py +0 -0
  83. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/lambda_erp/utils.py +0 -0
  84. {lambda_erp-0.6.20 → lambda_erp-0.6.22}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-erp
3
- Version: 0.6.20
3
+ Version: 0.6.22
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: Apache-2.0
@@ -1,5 +1,7 @@
1
1
  """Map lambda_erp exceptions to HTTP responses."""
2
2
 
3
+ import re
4
+
3
5
  from fastapi import FastAPI, Request
4
6
  from fastapi.responses import JSONResponse
5
7
  from lambda_erp.exceptions import (
@@ -14,6 +16,56 @@ from lambda_erp.exceptions import (
14
16
  )
15
17
 
16
18
 
19
+ # --- DB integrity violations -------------------------------------------------
20
+ # A unique/foreign-key violation raised by the driver (psycopg on Postgres,
21
+ # sqlite3 on SQLite) has no handler below, so it fell through to FastAPI's opaque
22
+ # 500 "Internal Server Error" — which a form may show as a stray red line or
23
+ # swallow entirely (e.g. "saving the UID does nothing, no warning"). Map it to a
24
+ # clear 409 with the colliding field/value so the UI can actually explain it.
25
+
26
+ _PG_KEY_RE = re.compile(r"Key \(([^)]+)\)=\(([^)]*)\)")
27
+ _SQLITE_UNIQUE_RE = re.compile(r"UNIQUE constraint failed: [^.]+\.([^\s,]+)")
28
+
29
+
30
+ def _integrity_error_types() -> list:
31
+ """The driver IntegrityError classes that are actually installed (SQLite
32
+ always; psycopg only with the [postgres] extra). Import defensively so a
33
+ SQLite-only deployment doesn't need psycopg."""
34
+ types: list = []
35
+ try:
36
+ import sqlite3
37
+ types.append(sqlite3.IntegrityError)
38
+ except Exception:
39
+ pass
40
+ for mod_name in ("psycopg", "psycopg2"):
41
+ try:
42
+ mod = __import__(mod_name)
43
+ err = getattr(mod, "IntegrityError", None) or getattr(
44
+ getattr(mod, "errors", None), "IntegrityError", None
45
+ )
46
+ if err is not None:
47
+ types.append(err)
48
+ except Exception:
49
+ pass
50
+ return types
51
+
52
+
53
+ def _friendly_integrity(msg: str) -> str:
54
+ m = _PG_KEY_RE.search(msg or "")
55
+ if m:
56
+ return f"A record with {m.group(1)} = “{m.group(2)}” already exists."
57
+ m = _SQLITE_UNIQUE_RE.search(msg or "")
58
+ if m:
59
+ return f"A record with this {m.group(1)} already exists."
60
+ if "foreign key" in (msg or "").lower():
61
+ return "This links to a record that doesn't exist."
62
+ return "A record with these details already exists (a unique field is duplicated)."
63
+
64
+
65
+ async def _integrity_handler(request: Request, exc: Exception):
66
+ return JSONResponse(status_code=409, content={"detail": _friendly_integrity(str(exc))})
67
+
68
+
17
69
  def register_exception_handlers(app: FastAPI):
18
70
 
19
71
  @app.exception_handler(ValueError)
@@ -54,3 +106,6 @@ def register_exception_handlers(app: FastAPI):
54
106
  @app.exception_handler(InsufficientFunds)
55
107
  async def insufficient_funds_error(request: Request, exc: InsufficientFunds):
56
108
  return JSONResponse(status_code=422, content={"detail": str(exc)})
109
+
110
+ for _exc_type in _integrity_error_types():
111
+ app.add_exception_handler(_exc_type, _integrity_handler)
@@ -319,9 +319,12 @@ def search_masters(master_type: str, q: str = "", _user: dict = _viewer):
319
319
  if not q:
320
320
  return db.get_all(doctype, filters=_with_active_filter(db, doctype), fields=["name", name_field], limit=10)
321
321
 
322
+ # Case-INSENSITIVE: on Postgres plain LIKE is case-sensitive, so "plus medica"
323
+ # would miss "Plus Medica AG". LOWER(...) LIKE LOWER(...) is portable (SQLite
324
+ # + Postgres) and matches how the document search (_search_clause) works.
322
325
  rows = db.sql(
323
326
  f'SELECT name, "{name_field}" FROM "{doctype}" '
324
- f'WHERE {active_prefix}(name LIKE ? OR "{name_field}" LIKE ?) LIMIT 10',
327
+ f'WHERE {active_prefix}(LOWER(name) LIKE LOWER(?) OR LOWER("{name_field}") LIKE LOWER(?)) LIMIT 10',
325
328
  [f"%{q}%", f"%{q}%"],
326
329
  )
327
330
  return rows
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.6.20"
3
+ version = "0.6.22"
4
4
  description = "Core ERP logic - accounting, sales, purchasing, inventory"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
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