lambda-erp 0.6.4__tar.gz → 0.6.6__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.6}/PKG-INFO +1 -1
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/routers/documents.py +13 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/routers/masters.py +26 -1
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/services.py +23 -4
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/frontend/src/api/client.ts +6 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/database.py +42 -7
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/pyproject.toml +1 -1
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/.gitignore +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/LICENSE +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/README.md +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/attachments.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/auth.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/bootstrap.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/chat.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/demo_limits.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/deps.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/errors.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/main.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/oauth.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/pdf.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/providers.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/remarks_md.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/routers/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/routers/accounting.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/routers/admin.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/routers/analytics.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/routers/proposals.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/routers/reports.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/routers/setup.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/templates/document.html +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/api/templates/proposal.html +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/docs/agents/README.md +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/frontend/LICENSE +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/frontend/README.md +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/model.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.6.4 → lambda_erp-0.6.6}/terraform/README.md +0 -0
|
@@ -28,6 +28,7 @@ _manager = Depends(require_role("manager"))
|
|
|
28
28
|
_LIST_RESERVED = {
|
|
29
29
|
"status", "party", "from_date", "to_date", "docstatus",
|
|
30
30
|
"include_discarded", "limit", "offset", "order_by", "order",
|
|
31
|
+
"date_field",
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
|
|
@@ -43,6 +44,7 @@ def list_docs(
|
|
|
43
44
|
include_discarded: bool = False,
|
|
44
45
|
order_by: str | None = None,
|
|
45
46
|
order: str = "desc",
|
|
47
|
+
date_field: str | None = None,
|
|
46
48
|
limit: int = Query(default=50, le=500),
|
|
47
49
|
offset: int = Query(default=0, ge=0),
|
|
48
50
|
_user: dict = _viewer,
|
|
@@ -70,6 +72,14 @@ def list_docs(
|
|
|
70
72
|
raise HTTPException(status_code=400, detail=f"Unknown filter field: {key}")
|
|
71
73
|
filters[key] = value
|
|
72
74
|
|
|
75
|
+
# Which column from_date/to_date filter on. The frontend passes its declared
|
|
76
|
+
# dateField so plugin doctypes get working date filters without a server-side
|
|
77
|
+
# map. Passed through untouched here; list_documents ignores it unless it's a
|
|
78
|
+
# real column (so a config with a synthetic dateField degrades to no date
|
|
79
|
+
# filter rather than 400-ing the whole list), and never interpolates it blind.
|
|
80
|
+
if date_field:
|
|
81
|
+
filters["date_field"] = date_field
|
|
82
|
+
|
|
73
83
|
if order_by is not None and order_by not in columns:
|
|
74
84
|
raise HTTPException(status_code=400, detail=f"Unknown order_by field: {order_by}")
|
|
75
85
|
if order.lower() not in ("asc", "desc"):
|
|
@@ -94,6 +104,7 @@ def adjacent_doc(
|
|
|
94
104
|
include_discarded: bool = False,
|
|
95
105
|
order_by: str | None = None,
|
|
96
106
|
order: str = "desc",
|
|
107
|
+
date_field: str | None = None,
|
|
97
108
|
_user: dict = _viewer,
|
|
98
109
|
):
|
|
99
110
|
"""Prev/next record around `name` in the same order+filters the list uses.
|
|
@@ -117,6 +128,8 @@ def adjacent_doc(
|
|
|
117
128
|
if key not in columns:
|
|
118
129
|
raise HTTPException(status_code=400, detail=f"Unknown filter field: {key}")
|
|
119
130
|
filters[key] = value
|
|
131
|
+
if date_field:
|
|
132
|
+
filters["date_field"] = date_field
|
|
120
133
|
if order_by is not None and order_by not in columns:
|
|
121
134
|
raise HTTPException(status_code=400, detail=f"Unknown order_by field: {order_by}")
|
|
122
135
|
if order.lower() not in ("asc", "desc"):
|
|
@@ -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)
|
|
@@ -392,6 +392,17 @@ DATE_FIELDS = {
|
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
|
|
395
|
+
def _resolve_date_field(db, doctype: str, override: str | None) -> str | None:
|
|
396
|
+
"""The column that from_date/to_date range-filter against. An explicit
|
|
397
|
+
override (the frontend's declared `dateField`, passed as the `date_field`
|
|
398
|
+
filter) wins when it names a real column; otherwise fall back to the
|
|
399
|
+
built-in DATE_FIELDS map for core doctypes. Guarded so the override is never
|
|
400
|
+
interpolated into SQL unless it's an actual column."""
|
|
401
|
+
if override and override in db._get_table_columns(doctype):
|
|
402
|
+
return override
|
|
403
|
+
return DATE_FIELDS.get(doctype)
|
|
404
|
+
|
|
405
|
+
|
|
395
406
|
def _exclude_discarded(db, doctype: str, db_filters: dict, include_discarded: bool) -> None:
|
|
396
407
|
"""Hide voided drafts (discarded=1) unless explicitly requested. Guarded on
|
|
397
408
|
the column existing so non-submittable doctypes are unaffected."""
|
|
@@ -421,6 +432,7 @@ def list_documents(doctype_slug: str, filters: dict = None, limit: int = 50, off
|
|
|
421
432
|
db_filters = {}
|
|
422
433
|
from_date = None
|
|
423
434
|
to_date = None
|
|
435
|
+
date_field_override = None
|
|
424
436
|
if filters:
|
|
425
437
|
for key, value in filters.items():
|
|
426
438
|
if value is None or value == "":
|
|
@@ -429,13 +441,15 @@ def list_documents(doctype_slug: str, filters: dict = None, limit: int = 50, off
|
|
|
429
441
|
from_date = value
|
|
430
442
|
elif key == "to_date":
|
|
431
443
|
to_date = value
|
|
444
|
+
elif key == "date_field":
|
|
445
|
+
date_field_override = value
|
|
432
446
|
else:
|
|
433
447
|
db_filters[key] = value
|
|
434
448
|
|
|
435
449
|
_exclude_discarded(db, doctype, db_filters, include_discarded)
|
|
436
450
|
|
|
437
451
|
# Date range filtering via the doctype's primary date field
|
|
438
|
-
date_field =
|
|
452
|
+
date_field = _resolve_date_field(db, doctype, date_field_override)
|
|
439
453
|
if date_field:
|
|
440
454
|
if from_date:
|
|
441
455
|
db_filters[date_field] = (">=", from_date)
|
|
@@ -481,6 +495,7 @@ def count_documents(doctype_slug: str, filters: dict = None, include_discarded:
|
|
|
481
495
|
db_filters = {}
|
|
482
496
|
from_date = None
|
|
483
497
|
to_date = None
|
|
498
|
+
date_field_override = None
|
|
484
499
|
if filters:
|
|
485
500
|
for key, value in filters.items():
|
|
486
501
|
if value is None or value == "":
|
|
@@ -489,12 +504,14 @@ def count_documents(doctype_slug: str, filters: dict = None, include_discarded:
|
|
|
489
504
|
from_date = value
|
|
490
505
|
elif key == "to_date":
|
|
491
506
|
to_date = value
|
|
507
|
+
elif key == "date_field":
|
|
508
|
+
date_field_override = value
|
|
492
509
|
else:
|
|
493
510
|
db_filters[key] = value
|
|
494
511
|
|
|
495
512
|
_exclude_discarded(db, doctype, db_filters, include_discarded)
|
|
496
513
|
|
|
497
|
-
date_field =
|
|
514
|
+
date_field = _resolve_date_field(db, doctype, date_field_override)
|
|
498
515
|
where_parts = []
|
|
499
516
|
params = []
|
|
500
517
|
if date_field and from_date:
|
|
@@ -524,7 +541,7 @@ def _filter_where(db, doctype: str, filters: dict, include_discarded: bool):
|
|
|
524
541
|
(equality filters + from_date/to_date on the doctype's date field + discard
|
|
525
542
|
exclusion). Same semantics as list_documents/count_documents so prev/next
|
|
526
543
|
matches exactly what the list shows."""
|
|
527
|
-
db_filters, from_date, to_date = {}, None, None
|
|
544
|
+
db_filters, from_date, to_date, date_field_override = {}, None, None, None
|
|
528
545
|
for key, value in (filters or {}).items():
|
|
529
546
|
if value is None or value == "":
|
|
530
547
|
continue
|
|
@@ -532,11 +549,13 @@ def _filter_where(db, doctype: str, filters: dict, include_discarded: bool):
|
|
|
532
549
|
from_date = value
|
|
533
550
|
elif key == "to_date":
|
|
534
551
|
to_date = value
|
|
552
|
+
elif key == "date_field":
|
|
553
|
+
date_field_override = value
|
|
535
554
|
else:
|
|
536
555
|
db_filters[key] = value
|
|
537
556
|
_exclude_discarded(db, doctype, db_filters, include_discarded)
|
|
538
557
|
|
|
539
|
-
date_field =
|
|
558
|
+
date_field = _resolve_date_field(db, doctype, date_field_override)
|
|
540
559
|
where_parts, params = [], []
|
|
541
560
|
if date_field and from_date:
|
|
542
561
|
where_parts.append(f'"{date_field}" >= ?'); params.append(from_date)
|
|
@@ -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
|