lambda-erp 0.8.6__tar.gz → 0.8.8__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.8.6 → lambda_erp-0.8.8}/PKG-INFO +1 -1
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/routers/documents.py +8 -7
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/services.py +16 -14
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/database.py +39 -1
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/pyproject.toml +1 -1
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/.gitignore +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/LICENSE +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/README.md +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/__init__.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/attachments.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/auth.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/bootstrap.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/chat.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/demo_limits.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/deps.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/errors.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/main.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/oauth.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/pdf.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/providers.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/remarks_md.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/routers/__init__.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/routers/accounting.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/routers/admin.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/routers/analytics.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/routers/availability.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/routers/masters.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/routers/mcp.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/routers/proposals.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/routers/reports.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/routers/setup.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/templates/document.html +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/api/templates/proposal.html +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/docs/agents/README.md +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/frontend/LICENSE +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/frontend/README.md +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/frontend/src/api/client.ts +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/assets/__init__.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/assets/asset.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/assets/reservation.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/model.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.8.6 → lambda_erp-0.8.8}/terraform/README.md +0 -0
|
@@ -100,15 +100,16 @@ def list_docs(
|
|
|
100
100
|
if order.lower() not in ("asc", "desc"):
|
|
101
101
|
raise HTTPException(status_code=400, detail="order must be 'asc' or 'desc'")
|
|
102
102
|
|
|
103
|
-
# Column projection: the frontend sends the columns its list view
|
|
104
|
-
#
|
|
105
|
-
#
|
|
103
|
+
# Column projection: the frontend sends the columns its list view renders, so
|
|
104
|
+
# a wide doctype doesn't ship every column for a few-column grid. This is
|
|
105
|
+
# ADVISORY — unknown columns are silently dropped, never a 400: the frontend
|
|
106
|
+
# appends display-only fields (currency, party_type) that not every doctype
|
|
107
|
+
# has, and a projection can only ever narrow the response (name is kept
|
|
108
|
+
# downstream), so an unknown field is harmless. (Contrast order_by/search_
|
|
109
|
+
# fields, which affect query correctness and stay strict.)
|
|
106
110
|
projection = None
|
|
107
111
|
if fields:
|
|
108
|
-
projection = [f.strip() for f in fields.split(",") if f.strip()]
|
|
109
|
-
bad = [f for f in projection if f not in columns]
|
|
110
|
-
if bad:
|
|
111
|
-
raise HTTPException(status_code=400, detail=f"Unknown field(s): {', '.join(bad)}")
|
|
112
|
+
projection = [f.strip() for f in fields.split(",") if f.strip() and f.strip() in columns]
|
|
112
113
|
|
|
113
114
|
rows = list_documents(doctype_slug, filters=filters, limit=limit, offset=offset,
|
|
114
115
|
include_discarded=include_discarded, order_by=order_by, order=order,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import time
|
|
4
4
|
|
|
5
5
|
from lambda_erp.utils import _dict, now
|
|
6
|
-
from lambda_erp.database import get_db
|
|
6
|
+
from lambda_erp.database import get_db, get_write_generation
|
|
7
7
|
|
|
8
8
|
from lambda_erp.selling.quotation import (
|
|
9
9
|
Quotation, make_sales_order,
|
|
@@ -736,13 +736,13 @@ def list_documents(doctype_slug: str, filters: dict = None, limit: int = 50, off
|
|
|
736
736
|
return rows
|
|
737
737
|
|
|
738
738
|
|
|
739
|
-
# Exact list counts are cached
|
|
740
|
-
#
|
|
741
|
-
#
|
|
742
|
-
#
|
|
743
|
-
#
|
|
744
|
-
#
|
|
745
|
-
#
|
|
739
|
+
# Exact list counts are cached per (doctype, filters). The count is the one O(N)
|
|
740
|
+
# part of a list load — a full scan even with the creation index, since the
|
|
741
|
+
# discard/search predicates aren't covered — and paging within a view repeats the
|
|
742
|
+
# identical count. The cache stays EXACT (unlike a reltuples estimate, which would
|
|
743
|
+
# make the page-jump "of N" bound approximate): every entry is tagged with the DB
|
|
744
|
+
# write-generation, so any in-process write invalidates it immediately; the TTL is
|
|
745
|
+
# only a backstop for cross-replica writes this process didn't see.
|
|
746
746
|
_COUNT_CACHE: dict = {}
|
|
747
747
|
_COUNT_CACHE_TTL = 60.0
|
|
748
748
|
_COUNT_CACHE_MAX = 512
|
|
@@ -754,22 +754,24 @@ def _count_cache_key(doctype_slug, filters, include_discarded):
|
|
|
754
754
|
|
|
755
755
|
|
|
756
756
|
def invalidate_count_cache() -> None:
|
|
757
|
-
"""Clear the list-count cache (
|
|
758
|
-
|
|
757
|
+
"""Clear the list-count cache (writes already invalidate via the write-
|
|
758
|
+
generation; call this only to force-drop cross-replica staleness early)."""
|
|
759
759
|
_COUNT_CACHE.clear()
|
|
760
760
|
|
|
761
761
|
|
|
762
762
|
def count_documents(doctype_slug: str, filters: dict = None, include_discarded: bool = False) -> int:
|
|
763
|
-
"""Exact count of documents matching the filters, cached
|
|
763
|
+
"""Exact count of documents matching the filters, cached per filter-set until
|
|
764
|
+
the next write (write-generation) or the TTL, whichever comes first."""
|
|
764
765
|
key = _count_cache_key(doctype_slug, filters, include_discarded)
|
|
766
|
+
gen = get_write_generation()
|
|
765
767
|
now_mono = time.monotonic()
|
|
766
768
|
hit = _COUNT_CACHE.get(key)
|
|
767
|
-
if hit is not None and now_mono - hit[0] < _COUNT_CACHE_TTL:
|
|
768
|
-
return hit[
|
|
769
|
+
if hit is not None and hit[1] == gen and now_mono - hit[0] < _COUNT_CACHE_TTL:
|
|
770
|
+
return hit[2]
|
|
769
771
|
result = _count_documents_uncached(doctype_slug, filters, include_discarded)
|
|
770
772
|
if len(_COUNT_CACHE) >= _COUNT_CACHE_MAX:
|
|
771
773
|
_COUNT_CACHE.clear()
|
|
772
|
-
_COUNT_CACHE[key] = (now_mono, result)
|
|
774
|
+
_COUNT_CACHE[key] = (now_mono, gen, result)
|
|
773
775
|
return result
|
|
774
776
|
|
|
775
777
|
|
|
@@ -96,6 +96,28 @@ def _pg_row_factory(cursor):
|
|
|
96
96
|
return make
|
|
97
97
|
|
|
98
98
|
|
|
99
|
+
_write_generation = 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _stmt_is_read(sql: str) -> bool:
|
|
103
|
+
s = sql.lstrip().lower()
|
|
104
|
+
return s.startswith("select") and "for update" not in s and "for share" not in s
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _note_write(sql: str) -> None:
|
|
108
|
+
"""Bump the process write-generation on any non-SELECT statement. The list
|
|
109
|
+
count cache keys on it, so an in-process write invalidates cached counts at
|
|
110
|
+
once (cross-process staleness stays bounded by the cache TTL)."""
|
|
111
|
+
global _write_generation
|
|
112
|
+
if not _stmt_is_read(sql):
|
|
113
|
+
_write_generation += 1
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_write_generation() -> int:
|
|
117
|
+
"""Monotonic per-process write counter — see _note_write."""
|
|
118
|
+
return _write_generation
|
|
119
|
+
|
|
120
|
+
|
|
99
121
|
class _PgConn:
|
|
100
122
|
"""Adapts a psycopg (v3) connection to the sqlite3 connection API the code
|
|
101
123
|
uses. autocommit is off so the existing `_in_transaction` + single commit()
|
|
@@ -150,6 +172,7 @@ class _PgConn:
|
|
|
150
172
|
self._safe_rollback()
|
|
151
173
|
raise
|
|
152
174
|
self._maybe_release(sql)
|
|
175
|
+
_note_write(sql)
|
|
153
176
|
return cur
|
|
154
177
|
|
|
155
178
|
def executemany(self, sql, seq_params):
|
|
@@ -161,6 +184,7 @@ class _PgConn:
|
|
|
161
184
|
self._safe_rollback()
|
|
162
185
|
raise
|
|
163
186
|
self._dirty = True
|
|
187
|
+
_note_write(sql)
|
|
164
188
|
return cur
|
|
165
189
|
|
|
166
190
|
def _safe_rollback(self):
|
|
@@ -185,6 +209,20 @@ class _PgConn:
|
|
|
185
209
|
return self._raw.cursor(*args, **kwargs)
|
|
186
210
|
|
|
187
211
|
|
|
212
|
+
class _SqliteConn(sqlite3.Connection):
|
|
213
|
+
"""SQLite connection that bumps the write-generation like _PgConn does, so the
|
|
214
|
+
list count cache invalidates on writes under both backends (SQLite is a raw
|
|
215
|
+
connection where Postgres is wrapped)."""
|
|
216
|
+
|
|
217
|
+
def execute(self, sql, *args, **kwargs):
|
|
218
|
+
_note_write(sql)
|
|
219
|
+
return super().execute(sql, *args, **kwargs)
|
|
220
|
+
|
|
221
|
+
def executemany(self, sql, *args, **kwargs):
|
|
222
|
+
_note_write(sql)
|
|
223
|
+
return super().executemany(sql, *args, **kwargs)
|
|
224
|
+
|
|
225
|
+
|
|
188
226
|
class _NullLock:
|
|
189
227
|
"""No-op context manager used when SQLite-level locking is sufficient."""
|
|
190
228
|
|
|
@@ -240,7 +278,7 @@ class Database:
|
|
|
240
278
|
return conn
|
|
241
279
|
|
|
242
280
|
def _open_sqlite_conn(self):
|
|
243
|
-
conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
|
281
|
+
conn = sqlite3.connect(self.db_path, check_same_thread=False, factory=_SqliteConn)
|
|
244
282
|
conn.row_factory = sqlite3.Row
|
|
245
283
|
conn.execute(f"PRAGMA journal_mode={_journal_mode()}")
|
|
246
284
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|