lambda-erp 0.6.3__tar.gz → 0.6.5__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.6.3 → lambda_erp-0.6.5}/PKG-INFO +1 -1
  2. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/routers/documents.py +45 -0
  3. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/routers/masters.py +26 -1
  4. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/services.py +70 -0
  5. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/frontend/src/api/client.ts +12 -0
  6. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/database.py +42 -7
  7. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/pyproject.toml +1 -1
  8. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/.gitignore +0 -0
  9. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/LICENSE +0 -0
  10. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/README.md +0 -0
  11. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/__init__.py +0 -0
  12. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/attachments.py +0 -0
  13. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/auth.py +0 -0
  14. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/bootstrap.py +0 -0
  15. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/chat.py +0 -0
  16. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/demo_limits.py +0 -0
  17. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/deps.py +0 -0
  18. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/errors.py +0 -0
  19. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/main.py +0 -0
  20. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/oauth.py +0 -0
  21. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/pdf.py +0 -0
  22. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/providers.py +0 -0
  23. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/remarks_md.py +0 -0
  24. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/routers/__init__.py +0 -0
  25. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/routers/accounting.py +0 -0
  26. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/routers/admin.py +0 -0
  27. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/routers/analytics.py +0 -0
  28. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/routers/bank_reconciliation.py +0 -0
  29. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/routers/chat_api.py +0 -0
  30. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/routers/proposals.py +0 -0
  31. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/routers/reports.py +0 -0
  32. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/routers/setup.py +0 -0
  33. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/templates/document.html +0 -0
  34. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/api/templates/proposal.html +0 -0
  35. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/docs/agents/README.md +0 -0
  36. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/frontend/LICENSE +0 -0
  37. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/frontend/README.md +0 -0
  38. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/__init__.py +0 -0
  39. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/__init__.py +0 -0
  40. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/bank_transaction.py +0 -0
  41. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/budget.py +0 -0
  42. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  43. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/general_ledger.py +0 -0
  44. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/journal_entry.py +0 -0
  45. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/payment_entry.py +0 -0
  46. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/pos_invoice.py +0 -0
  47. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/purchase_invoice.py +0 -0
  48. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/revaluation.py +0 -0
  49. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/sales_invoice.py +0 -0
  50. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/__init__.py +0 -0
  51. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/engine.py +0 -0
  52. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/pack.py +0 -0
  53. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
  54. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/packs/ch.py +0 -0
  55. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
  56. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
  57. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
  58. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/packs/generic.py +0 -0
  59. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/profiles.py +0 -0
  60. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/spine.py +0 -0
  61. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/accounting/subscription.py +0 -0
  62. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/buying/__init__.py +0 -0
  63. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/buying/purchase_order.py +0 -0
  64. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/controllers/__init__.py +0 -0
  65. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/controllers/currency.py +0 -0
  66. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/controllers/defaults.py +0 -0
  67. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/controllers/pricing_rule.py +0 -0
  68. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  69. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/exceptions.py +0 -0
  70. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/hooks.py +0 -0
  71. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/model.py +0 -0
  72. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/selling/__init__.py +0 -0
  73. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/selling/proposal.py +0 -0
  74. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/selling/quotation.py +0 -0
  75. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/selling/sales_order.py +0 -0
  76. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/simulation.py +0 -0
  77. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/stock/__init__.py +0 -0
  78. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/stock/delivery_note.py +0 -0
  79. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/stock/purchase_receipt.py +0 -0
  80. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/stock/stock_entry.py +0 -0
  81. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/stock/stock_ledger.py +0 -0
  82. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/lambda_erp/utils.py +0 -0
  83. {lambda_erp-0.6.3 → lambda_erp-0.6.5}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-erp
3
- Version: 0.6.3
3
+ Version: 0.6.5
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: Apache-2.0
@@ -13,6 +13,7 @@ from api.services import (
13
13
  list_documents,
14
14
  count_documents,
15
15
  document_columns,
16
+ adjacent_documents,
16
17
  )
17
18
  from api.pdf import generate_pdf
18
19
  from api.auth import require_role
@@ -80,6 +81,50 @@ def list_docs(
80
81
  return {"rows": rows, "total": total, "limit": limit, "offset": offset}
81
82
 
82
83
 
84
+ @router.get("/{doctype_slug}/{name}/adjacent")
85
+ def adjacent_doc(
86
+ doctype_slug: str,
87
+ name: str,
88
+ request: Request,
89
+ status: str | None = None,
90
+ party: str | None = None,
91
+ from_date: str | None = None,
92
+ to_date: str | None = None,
93
+ docstatus: int | None = None,
94
+ include_discarded: bool = False,
95
+ order_by: str | None = None,
96
+ order: str = "desc",
97
+ _user: dict = _viewer,
98
+ ):
99
+ """Prev/next record around `name` in the same order+filters the list uses.
100
+ Returns {"prev": name|None, "next": name|None}. Filters mirror the list
101
+ endpoint so navigation stays within the list the user came from."""
102
+ filters = {}
103
+ if status:
104
+ filters["status"] = status
105
+ if docstatus is not None:
106
+ filters["docstatus"] = docstatus
107
+ if party:
108
+ filters["customer"] = party
109
+ if from_date:
110
+ filters["from_date"] = from_date
111
+ if to_date:
112
+ filters["to_date"] = to_date
113
+ columns = document_columns(doctype_slug)
114
+ for key, value in request.query_params.items():
115
+ if key in _LIST_RESERVED:
116
+ continue
117
+ if key not in columns:
118
+ raise HTTPException(status_code=400, detail=f"Unknown filter field: {key}")
119
+ filters[key] = value
120
+ if order_by is not None and order_by not in columns:
121
+ raise HTTPException(status_code=400, detail=f"Unknown order_by field: {order_by}")
122
+ if order.lower() not in ("asc", "desc"):
123
+ raise HTTPException(status_code=400, detail="order must be 'asc' or 'desc'")
124
+ return adjacent_documents(doctype_slug, name, filters=filters,
125
+ include_discarded=include_discarded, order_by=order_by, order=order)
126
+
127
+
83
128
  @router.get("/{doctype_slug}/search")
84
129
  def search_docs(doctype_slug: str, q: str = "", limit: int = Query(default=10, le=50), _user: dict = _viewer):
85
130
  docs = list_documents(doctype_slug, limit=limit)
@@ -299,7 +299,9 @@ def list_masters(
299
299
  query = f'SELECT * FROM "{doctype}"'
300
300
  if where_parts:
301
301
  query += " WHERE " + " AND ".join(where_parts)
302
- query += f" LIMIT {int(limit)}"
302
+ # Deterministic order: without it Postgres returns pages in arbitrary order,
303
+ # which breaks OFFSET pagination and the /adjacent prev/next contract.
304
+ query += f" ORDER BY name LIMIT {int(limit)}"
303
305
  if offset:
304
306
  query += f" OFFSET {int(offset)}"
305
307
  rows = db.sql(query, params)
@@ -325,6 +327,29 @@ def search_masters(master_type: str, q: str = "", _user: dict = _viewer):
325
327
  return rows
326
328
 
327
329
 
330
+ @router.get("/{master_type}/{name}/adjacent")
331
+ def adjacent_master(master_type: str, name: str, include_disabled: bool = True,
332
+ _user: dict = _viewer):
333
+ """The records immediately before/after `name` in the list's order (name
334
+ ASC) — {"prev": name|None, "next": name|None}. Keyset queries on the
335
+ primary key. include_disabled defaults True to step 1:1 with the master
336
+ list, which shows disabled records too."""
337
+ doctype, _ = _get_table(master_type)
338
+ if not doctype:
339
+ raise HTTPException(status_code=404, detail=f"Unknown master type: {master_type}")
340
+ db = get_db()
341
+ active = ("" if include_disabled or "disabled" not in db._get_table_columns(doctype)
342
+ else "disabled = 0 AND ")
343
+
344
+ def neighbor(cmp: str, direction: str):
345
+ rows = db.sql(
346
+ f'SELECT name FROM "{doctype}" WHERE {active}name {cmp} ? '
347
+ f"ORDER BY name {direction} LIMIT 1", [name])
348
+ return rows[0]["name"] if rows else None
349
+
350
+ return {"prev": neighbor("<", "DESC"), "next": neighbor(">", "ASC")}
351
+
352
+
328
353
  @router.get("/{master_type}/{name}")
329
354
  def get_master(master_type: str, name: str, _user: dict = _viewer):
330
355
  doctype, _ = _get_table(master_type)
@@ -519,6 +519,76 @@ def count_documents(doctype_slug: str, filters: dict = None, include_discarded:
519
519
  return int(rows[0]["c"]) if rows else 0
520
520
 
521
521
 
522
+ def _filter_where(db, doctype: str, filters: dict, include_discarded: bool):
523
+ """Build (where_parts, params) for a doctype from a list-style `filters` dict
524
+ (equality filters + from_date/to_date on the doctype's date field + discard
525
+ exclusion). Same semantics as list_documents/count_documents so prev/next
526
+ matches exactly what the list shows."""
527
+ db_filters, from_date, to_date = {}, None, None
528
+ for key, value in (filters or {}).items():
529
+ if value is None or value == "":
530
+ continue
531
+ if key == "from_date":
532
+ from_date = value
533
+ elif key == "to_date":
534
+ to_date = value
535
+ else:
536
+ db_filters[key] = value
537
+ _exclude_discarded(db, doctype, db_filters, include_discarded)
538
+
539
+ date_field = DATE_FIELDS.get(doctype)
540
+ where_parts, params = [], []
541
+ if date_field and from_date:
542
+ where_parts.append(f'"{date_field}" >= ?'); params.append(from_date)
543
+ if date_field and to_date:
544
+ where_parts.append(f'"{date_field}" <= ?'); params.append(to_date)
545
+ for k, v in db_filters.items():
546
+ if isinstance(v, (list, tuple)) and len(v) == 2:
547
+ op, val = v
548
+ where_parts.append(f'"{k}" {op} ?'); params.append(val)
549
+ else:
550
+ where_parts.append(f'"{k}" = ?'); params.append(v)
551
+ return where_parts, params
552
+
553
+
554
+ def adjacent_documents(doctype_slug: str, name: str, filters: dict = None,
555
+ include_discarded: bool = False, order_by: str = None,
556
+ order: str = "desc") -> dict:
557
+ """The records immediately before/after `name` in the same order+filters the
558
+ list uses — {"prev": name|None, "next": name|None}. `prev` is one step *up*
559
+ the list (toward the top), `next` one step *down*. Keyset queries (indexed,
560
+ not a full scan). Order defaults to creation DESC (the list default), with
561
+ `name` as a stable tie-break so rows sharing an order value still step 1:1."""
562
+ doctype = SLUG_TO_DOCTYPE.get(doctype_slug)
563
+ if not doctype:
564
+ raise ValueError(f"Unknown document type: {doctype_slug}")
565
+ db = get_db()
566
+ cols = db._get_table_columns(doctype)
567
+ oc = order_by if (order_by and order_by in cols) else "creation"
568
+ desc = str(order).lower() != "asc"
569
+
570
+ cur = db.sql(f'SELECT "{oc}" AS oc FROM "{doctype}" WHERE name = ?', [name])
571
+ if not cur:
572
+ return {"prev": None, "next": None}
573
+ cur_oc = cur[0]["oc"]
574
+
575
+ base_where, base_params = _filter_where(db, doctype, filters or {}, include_discarded)
576
+
577
+ def neighbor(direction):
578
+ # DESC list: next = tuple < current, prev = tuple > current (ASC flips).
579
+ going_less = (direction == "next") == desc
580
+ cmp = "<" if going_less else ">"
581
+ dir_sql = "DESC" if going_less else "ASC"
582
+ keyset = f'(("{oc}" {cmp} ?) OR ("{oc}" = ? AND name {cmp} ?))'
583
+ query = (f'SELECT name FROM "{doctype}" WHERE '
584
+ + " AND ".join(base_where + [keyset])
585
+ + f' ORDER BY "{oc}" {dir_sql}, name {dir_sql} LIMIT 1')
586
+ rows = db.sql(query, base_params + [cur_oc, cur_oc, name])
587
+ return rows[0]["name"] if rows else None
588
+
589
+ return {"prev": neighbor("prev"), "next": neighbor("next")}
590
+
591
+
522
592
  def _list_with_offset(db, doctype, doctype_slug, db_filters, limit, offset, order_clause="creation DESC"):
523
593
  where_parts = []
524
594
  params = []
@@ -123,6 +123,18 @@ export const api = {
123
123
  getDocument: (doctype: string, name: string) =>
124
124
  request<any>(`/documents/${doctype}/${encodeURIComponent(name)}`),
125
125
 
126
+ // Prev/next record around `name` in the same order+filters the list uses.
127
+ adjacentDocument: (doctype: string, name: string, params?: Record<string, string | number | undefined>) =>
128
+ request<{ prev: string | null; next: string | null }>(
129
+ `/documents/${doctype}/${encodeURIComponent(name)}/adjacent${qs(params)}`,
130
+ ),
131
+
132
+ // Prev/next master record around `name` in the master list's order (name ASC).
133
+ adjacentMaster: (masterType: string, name: string) =>
134
+ request<{ prev: string | null; next: string | null }>(
135
+ `/masters/${masterType}/${encodeURIComponent(name)}/adjacent`,
136
+ ),
137
+
126
138
  createDocument: (doctype: string, data: any) =>
127
139
  request<any>(`/documents/${doctype}`, { method: "POST", body: JSON.stringify(data) }),
128
140
 
@@ -99,10 +99,38 @@ def _pg_row_factory(cursor):
99
99
  class _PgConn:
100
100
  """Adapts a psycopg (v3) connection to the sqlite3 connection API the code
101
101
  uses. autocommit is off so the existing `_in_transaction` + single commit()
102
- / rollback() flow gives the same atomicity as SQLite's implicit transaction."""
103
-
104
- def __init__(self, raw):
102
+ / rollback() flow gives the same atomicity as SQLite's implicit transaction.
103
+
104
+ With autocommit off, even a plain SELECT opens a transaction that stays
105
+ open until commit/rollback — and these thread-local connections live as
106
+ long as their (pooled) thread, so a request whose last statement was a read
107
+ left the connection 'idle in transaction' forever, pinning ACCESS SHARE
108
+ locks on every table it touched. That starves any ALTER TABLE of its
109
+ ACCESS EXCLUSIVE lock, which made lock-safe plugin migrations time out on
110
+ every boot in production. The fix: after a read, when there is no pending
111
+ write and no explicit transaction, end the implicit transaction right away
112
+ (_maybe_release), so connections never idle inside one."""
113
+
114
+ def __init__(self, raw, in_explicit_txn=None):
105
115
  self._raw = raw
116
+ self._dirty = False # uncommitted write on this connection
117
+ self._in_explicit_txn = in_explicit_txn or (lambda: False)
118
+
119
+ @staticmethod
120
+ def _is_read(sql):
121
+ s = sql.lstrip().lower()
122
+ return s.startswith("select") and "for update" not in s and "for share" not in s
123
+
124
+ def _maybe_release(self, sql):
125
+ if self._in_explicit_txn():
126
+ return
127
+ if self._is_read(sql):
128
+ # psycopg's default cursor has already buffered the full result set
129
+ # client-side, so the caller can still fetch after the rollback.
130
+ if not self._dirty:
131
+ self._safe_rollback()
132
+ else:
133
+ self._dirty = True
106
134
 
107
135
  def execute(self, sql, params=None):
108
136
  try:
@@ -114,7 +142,6 @@ class _PgConn:
114
142
  cur.execute(sql.replace("%", "%%").replace("?", "%s"), list(params))
115
143
  else:
116
144
  cur.execute(sql.replace("?", "%s"))
117
- return cur
118
145
  except Exception:
119
146
  # A failed statement leaves the connection in an aborted transaction
120
147
  # (autocommit=False); every later query on this pooled/thread-local
@@ -122,28 +149,34 @@ class _PgConn:
122
149
  # so the connection stays usable, then re-raise the real error.
123
150
  self._safe_rollback()
124
151
  raise
152
+ self._maybe_release(sql)
153
+ return cur
125
154
 
126
155
  def executemany(self, sql, seq_params):
127
156
  try:
128
157
  cur = self._raw.cursor()
129
158
  cur.executemany(sql.replace("%", "%%").replace("?", "%s"),
130
159
  [list(p) for p in seq_params])
131
- return cur
132
160
  except Exception:
133
161
  self._safe_rollback()
134
162
  raise
163
+ self._dirty = True
164
+ return cur
135
165
 
136
166
  def _safe_rollback(self):
137
167
  try:
138
168
  self._raw.rollback()
169
+ self._dirty = False
139
170
  except Exception:
140
171
  pass
141
172
 
142
173
  def commit(self):
143
174
  self._raw.commit()
175
+ self._dirty = False
144
176
 
145
177
  def rollback(self):
146
178
  self._raw.rollback()
179
+ self._dirty = False
147
180
 
148
181
  def close(self):
149
182
  self._raw.close()
@@ -224,9 +257,11 @@ class Database:
224
257
 
225
258
  # autocommit off: matches SQLite's implicit transaction so the existing
226
259
  # _in_transaction + commit()/rollback() atomicity (GL submit/cancel) is
227
- # preserved exactly.
260
+ # preserved exactly. in_explicit_txn lets the wrapper see the
261
+ # thread-local flag so reads inside submit/cancel keep their snapshot.
228
262
  return _PgConn(psycopg.connect(self.db_path, autocommit=False,
229
- row_factory=_pg_row_factory))
263
+ row_factory=_pg_row_factory),
264
+ in_explicit_txn=lambda: self._in_transaction)
230
265
 
231
266
  @property
232
267
  def conn(self):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.6.3"
3
+ version = "0.6.5"
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