lambda-erp 0.6.4__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.
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/PKG-INFO +1 -1
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/routers/masters.py +26 -1
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/frontend/src/api/client.ts +6 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/database.py +42 -7
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/pyproject.toml +1 -1
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/.gitignore +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/LICENSE +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/README.md +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/attachments.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/auth.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/bootstrap.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/chat.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/demo_limits.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/deps.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/errors.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/main.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/oauth.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/pdf.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/providers.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/remarks_md.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/routers/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/routers/accounting.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/routers/admin.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/routers/analytics.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/routers/documents.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/routers/proposals.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/routers/reports.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/routers/setup.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/services.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/templates/document.html +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/api/templates/proposal.html +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/docs/agents/README.md +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/frontend/LICENSE +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/frontend/README.md +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/model.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.5}/terraform/README.md +0 -0
|
@@ -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
|
-
|
|
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)
|
|
@@ -129,6 +129,12 @@ export const api = {
|
|
|
129
129
|
`/documents/${doctype}/${encodeURIComponent(name)}/adjacent${qs(params)}`,
|
|
130
130
|
),
|
|
131
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
|
+
|
|
132
138
|
createDocument: (doctype: string, data: any) =>
|
|
133
139
|
request<any>(`/documents/${doctype}`, { method: "POST", body: JSON.stringify(data) }),
|
|
134
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
|
-
|
|
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):
|
|
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
|
|
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
|