lambda-erp 0.3.0__tar.gz → 0.3.2__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 (83) hide show
  1. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/PKG-INFO +1 -1
  2. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/chat.py +23 -4
  3. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/routers/masters.py +8 -1
  4. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/journal_entry.py +28 -0
  5. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/pyproject.toml +1 -1
  6. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/.gitignore +0 -0
  7. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/LICENSE +0 -0
  8. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/README.md +0 -0
  9. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/__init__.py +0 -0
  10. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/attachments.py +0 -0
  11. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/auth.py +0 -0
  12. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/bootstrap.py +0 -0
  13. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/demo_limits.py +0 -0
  14. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/deps.py +0 -0
  15. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/errors.py +0 -0
  16. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/main.py +0 -0
  17. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/oauth.py +0 -0
  18. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/pdf.py +0 -0
  19. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/providers.py +0 -0
  20. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/remarks_md.py +0 -0
  21. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/routers/__init__.py +0 -0
  22. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/routers/accounting.py +0 -0
  23. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/routers/admin.py +0 -0
  24. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/routers/analytics.py +0 -0
  25. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/routers/bank_reconciliation.py +0 -0
  26. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/routers/chat_api.py +0 -0
  27. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/routers/documents.py +0 -0
  28. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/routers/proposals.py +0 -0
  29. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/routers/reports.py +0 -0
  30. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/routers/setup.py +0 -0
  31. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/services.py +0 -0
  32. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/templates/document.html +0 -0
  33. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/api/templates/proposal.html +0 -0
  34. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/docs/agents/README.md +0 -0
  35. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/frontend/LICENSE +0 -0
  36. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/frontend/README.md +0 -0
  37. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/frontend/src/api/client.ts +0 -0
  38. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/__init__.py +0 -0
  39. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/__init__.py +0 -0
  40. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/bank_transaction.py +0 -0
  41. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/budget.py +0 -0
  42. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  43. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/general_ledger.py +0 -0
  44. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/payment_entry.py +0 -0
  45. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/pos_invoice.py +0 -0
  46. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/purchase_invoice.py +0 -0
  47. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/revaluation.py +0 -0
  48. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/sales_invoice.py +0 -0
  49. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/setup/__init__.py +0 -0
  50. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/setup/engine.py +0 -0
  51. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/setup/pack.py +0 -0
  52. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
  53. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/setup/packs/ch.py +0 -0
  54. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
  55. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
  56. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
  57. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/setup/packs/generic.py +0 -0
  58. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/setup/profiles.py +0 -0
  59. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/setup/spine.py +0 -0
  60. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/accounting/subscription.py +0 -0
  61. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/buying/__init__.py +0 -0
  62. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/buying/purchase_order.py +0 -0
  63. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/controllers/__init__.py +0 -0
  64. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/controllers/currency.py +0 -0
  65. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/controllers/defaults.py +0 -0
  66. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/controllers/pricing_rule.py +0 -0
  67. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  68. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/database.py +0 -0
  69. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/exceptions.py +0 -0
  70. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/hooks.py +0 -0
  71. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/model.py +0 -0
  72. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/selling/__init__.py +0 -0
  73. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/selling/proposal.py +0 -0
  74. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/selling/quotation.py +0 -0
  75. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/selling/sales_order.py +0 -0
  76. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/simulation.py +0 -0
  77. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/stock/__init__.py +0 -0
  78. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/stock/delivery_note.py +0 -0
  79. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/stock/purchase_receipt.py +0 -0
  80. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/stock/stock_entry.py +0 -0
  81. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/stock/stock_ledger.py +0 -0
  82. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/lambda_erp/utils.py +0 -0
  83. {lambda_erp-0.3.0 → lambda_erp-0.3.2}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-erp
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: Apache-2.0
@@ -583,7 +583,7 @@ TOOLS = [
583
583
  "type": "function",
584
584
  "function": {
585
585
  "name": "search_masters",
586
- "description": "Search master data (customers, suppliers, items, warehouses, accounts, companies, cost centers). Case-insensitive, with fuzzy fallback for misspellings. By default matches across standard text fields (name, display name, address/city/zip); large free-text columns like description/templates are skipped unless named in `fields`. PREFER passing `fields` whenever you know which attribute you're matching on (e.g. a city, an email, a tax id) — it's faster, more precise, and avoids false hits from other columns. Returns matching records.",
586
+ "description": "Search master data (customers, suppliers, items, warehouses, accounts, companies, cost centers). Case-insensitive, with fuzzy fallback for misspellings. By default matches across standard text fields (name, display name, address/city/zip); large free-text columns like description/templates are skipped unless named in `fields`. PREFER passing `fields` whenever you know which attribute you're matching on (e.g. a city, an email, a tax id) — it's faster, more precise, and avoids false hits from other columns. Returns ALL matching records by default (no cap); pass `limit` only to bound a large list.",
587
587
  "parameters": {
588
588
  "type": "object",
589
589
  "properties": {
@@ -594,6 +594,7 @@ TOOLS = [
594
594
  "items": {"type": "string"},
595
595
  "description": "Recommended: the column(s) to search, e.g. [\"city\"] or [\"customer_name\"]. Narrowing here is faster and avoids matching unrelated columns. Omit only when you genuinely don't know which field holds the value, to search all standard text fields.",
596
596
  },
597
+ "limit": {"type": "integer", "description": "Optional max number of results. OMIT for no cap — returns ALL matches (e.g. to see the entire chart of accounts). Pass a number only to bound a large list."},
597
598
  },
598
599
  "required": ["master_type"],
599
600
  },
@@ -1308,6 +1309,17 @@ def _handle_search_masters(args):
1308
1309
  master_type = args["master_type"]
1309
1310
  query = (args.get("query") or "").strip()
1310
1311
 
1312
+ # Optional result cap the caller sets per call; omit for NO cap (return all —
1313
+ # e.g. the whole chart of accounts). Coerced to int so the LIMIT clause below
1314
+ # can't be injected.
1315
+ _raw_limit = args.get("limit")
1316
+ limit = None
1317
+ if _raw_limit not in (None, "", 0):
1318
+ try:
1319
+ limit = max(1, int(_raw_limit))
1320
+ except (TypeError, ValueError):
1321
+ limit = None
1322
+
1311
1323
  entry = services.MASTER_TABLES.get(master_type)
1312
1324
  if not entry:
1313
1325
  return {"error": f"Unknown master type: {master_type}"}
@@ -1317,7 +1329,7 @@ def _handle_search_masters(args):
1317
1329
 
1318
1330
  if not query:
1319
1331
  filters = {"disabled": 0} if has_disabled else None
1320
- return db.get_all(doctype, filters=filters, fields=["*"], limit=20)
1332
+ return db.get_all(doctype, filters=filters, fields=["*"], limit=limit)
1321
1333
 
1322
1334
  # Optional `fields` narrows the search to specific columns — cheaper, more
1323
1335
  # precise, and the only way to reach bulk columns (description, templates)
@@ -1338,15 +1350,16 @@ def _handle_search_masters(args):
1338
1350
  # silently broke prod search); CAST lets targeted non-text columns match too.
1339
1351
  where = " OR ".join(f'lower(CAST("{col}" AS TEXT)) LIKE ?' for col in search_cols)
1340
1352
  pattern = f"%{query.lower()}%"
1353
+ limit_sql = f" LIMIT {limit}" if limit else ""
1341
1354
  rows = db.sql(
1342
- f'SELECT * FROM "{doctype}" WHERE {active_prefix}({where}) LIMIT 20',
1355
+ f'SELECT * FROM "{doctype}" WHERE {active_prefix}({where}){limit_sql}',
1343
1356
  [pattern] * len(search_cols),
1344
1357
  )
1345
1358
  if rows:
1346
1359
  return [dict(r) for r in rows]
1347
1360
 
1348
1361
  # Nothing matched literally — try fuzzy matching to catch misspellings.
1349
- return _fuzzy_master_search(db, doctype, search_cols, query, has_disabled, limit=20)
1362
+ return _fuzzy_master_search(db, doctype, search_cols, query, has_disabled, limit=limit)
1350
1363
 
1351
1364
 
1352
1365
  def _ignored_master_fields(master_type: str, data: dict) -> list[str]:
@@ -1891,6 +1904,12 @@ If `jurisdiction.is_fallback` is true, tell the user their country isn't localiz
1891
1904
 
1892
1905
  You help users manage their business by creating documents, looking up data, and running reports — all through natural conversation.
1893
1906
 
1907
+ ## Never fabricate actions or results
1908
+ Only state that something was done — created, changed, enabled/disabled, booked, repointed — **after a tool call returns a success result that confirms it**, and check the returned record actually reflects the change (after `update_master` with {{"disabled": 0}}, confirm the returned row shows `disabled = 0`; after submitting a journal entry, confirm it posted non-zero GL). If a tool returns an error, a warning, or a record that doesn't reflect your intent, tell the user it did **not** work and why — never smooth it over as success.
1909
+ - If a capability doesn't exist, say so plainly. There is **no rename** for accounts or other masters — a master's identifying `name`/number is immutable; you can only edit display fields or create a new record and migrate. Never claim you "renamed" an account.
1910
+ - Never assert a field or capability is missing without checking (e.g. an item's income account) — verify with `get_master_fields` / `search_masters` first.
1911
+ - If you can't confirm an action took effect, re-fetch and look, or tell the user you couldn't confirm it — never guess.
1912
+
1894
1913
  ## Answering data questions — three paths
1895
1914
 
1896
1915
  **Path 1: single-record lookup → `list_documents` / `get_document`.** For "is SINV-0042 paid", "what did customer X order last", "show me the latest 5 purchase orders" — fetch the rows directly. Don't try to aggregate in your head unless there are only a handful of rows in front of you.
@@ -187,7 +187,14 @@ def _generate_master_name(db, doctype: str, prefix: str) -> str:
187
187
  def _normalize_master_data(data: dict) -> dict:
188
188
  normalized = _dict(data)
189
189
  for key, value in list(normalized.items()):
190
- if isinstance(value, str) and value.strip() == "":
190
+ # Flag columns (disabled, is_group, …) are INTEGER, not boolean. The chat
191
+ # may send a JSON bool; coerce true/false -> 1/0 so e.g. re-enabling an
192
+ # account with {"disabled": false} actually persists (on Postgres a bool
193
+ # into an INTEGER column errors, which then gets narrated as "done"). Check
194
+ # bool BEFORE int — in Python bool is a subclass of int.
195
+ if isinstance(value, bool):
196
+ normalized[key] = int(value)
197
+ elif isinstance(value, str) and value.strip() == "":
191
198
  normalized[key] = None
192
199
  return normalized
193
200
 
@@ -70,10 +70,33 @@ class JournalEntry(Document):
70
70
  if not self.posting_date:
71
71
  self.posting_date = nowdate()
72
72
 
73
+ self._normalize_amounts()
73
74
  self._validate_debit_credit()
74
75
  self._validate_references()
75
76
  self._set_totals()
76
77
 
78
+ def _normalize_amounts(self):
79
+ """Keep each row's base debit/credit and its *_in_account_currency twin in
80
+ sync. lambda-erp has no per-line exchange rate (base == account currency),
81
+ so back-fill whichever side is missing. Without this, a row created with
82
+ the amount ONLY in debit_in_account_currency (as the chat's create path
83
+ did) leaves base debit/credit at 0 — it passes the 0==0 balance check,
84
+ submits "successfully", and posts NOTHING (GL is built from the base
85
+ fields, and _get_gl_entries skips 0/0 rows). Idempotent."""
86
+ for row in self.get("accounts") or []:
87
+ debit = flt(row.get("debit"))
88
+ credit = flt(row.get("credit"))
89
+ dr_ac = flt(row.get("debit_in_account_currency"))
90
+ cr_ac = flt(row.get("credit_in_account_currency"))
91
+ if not debit and dr_ac:
92
+ row["debit"] = dr_ac
93
+ if not credit and cr_ac:
94
+ row["credit"] = cr_ac
95
+ if not dr_ac and debit:
96
+ row["debit_in_account_currency"] = debit
97
+ if not cr_ac and credit:
98
+ row["credit_in_account_currency"] = credit
99
+
77
100
  def _validate_debit_credit(self):
78
101
  """Ensure total debits == total credits.
79
102
 
@@ -100,6 +123,11 @@ class JournalEntry(Document):
100
123
  def on_submit(self):
101
124
  """Post GL entries directly from account rows."""
102
125
  gl_entries = self._get_gl_entries()
126
+ if not gl_entries:
127
+ raise ValidationError(
128
+ "Journal Entry has no non-zero lines to post — every account row is "
129
+ "0/0. Set a debit or credit amount before submitting."
130
+ )
103
131
  make_gl_entries(gl_entries)
104
132
  self._update_referenced_outstanding()
105
133
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.3.0"
3
+ version = "0.3.2"
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
File without changes
File without changes
File without changes
File without changes