lambda-erp 0.6.1__tar.gz → 0.6.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.
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/PKG-INFO +1 -1
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/database.py +73 -6
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/pyproject.toml +1 -1
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/.gitignore +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/LICENSE +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/README.md +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/__init__.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/attachments.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/auth.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/bootstrap.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/chat.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/demo_limits.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/deps.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/errors.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/main.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/oauth.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/pdf.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/providers.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/remarks_md.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/routers/__init__.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/routers/accounting.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/routers/admin.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/routers/analytics.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/routers/documents.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/routers/masters.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/routers/proposals.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/routers/reports.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/routers/setup.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/services.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/templates/document.html +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/api/templates/proposal.html +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/docs/agents/README.md +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/frontend/LICENSE +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/frontend/README.md +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/frontend/src/api/client.ts +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/model.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.6.1 → lambda_erp-0.6.2}/terraform/README.md +0 -0
|
@@ -1391,13 +1391,80 @@ class Database:
|
|
|
1391
1391
|
self._col_cache.pop(table, None)
|
|
1392
1392
|
self._text_col_cache.pop(table, None)
|
|
1393
1393
|
|
|
1394
|
+
def _alter_table_lock_safe(self, statements, cache_tables, *,
|
|
1395
|
+
lock_timeout_ms=3000, retries=8, backoff=0.5):
|
|
1396
|
+
"""Run ALTER TABLE DDL with a bounded lock wait + retry on Postgres.
|
|
1397
|
+
|
|
1398
|
+
A schema change (ADD/DROP COLUMN) needs an ACCESS EXCLUSIVE lock; during
|
|
1399
|
+
a rolling deploy the OLD app revision is still live and querying the
|
|
1400
|
+
table, so an unbounded ALTER blocks *indefinitely* — and because these
|
|
1401
|
+
migrations run in the app lifespan before it serves, a blocked ALTER
|
|
1402
|
+
fails the container's startup probe and crash-loops the revision (this
|
|
1403
|
+
actually happened in production). Bounding `lock_timeout` turns "hang
|
|
1404
|
+
forever" into "fail fast", and retrying catches a gap between the old
|
|
1405
|
+
revision's queries. If every attempt times out it raises, so the caller
|
|
1406
|
+
(e.g. the plugin migration runner) can retry on the next boot rather
|
|
1407
|
+
than hang. SQLite has no concurrent revision, so it runs directly."""
|
|
1408
|
+
if self.dialect != "postgres":
|
|
1409
|
+
for s in statements:
|
|
1410
|
+
self.conn.execute(self._ddl(s))
|
|
1411
|
+
self.conn.commit()
|
|
1412
|
+
for t in cache_tables:
|
|
1413
|
+
self._col_cache.pop(t, None)
|
|
1414
|
+
self._text_col_cache.pop(t, None)
|
|
1415
|
+
return
|
|
1416
|
+
|
|
1417
|
+
import time as _time
|
|
1418
|
+
last = None
|
|
1419
|
+
try:
|
|
1420
|
+
for _ in range(retries):
|
|
1421
|
+
try:
|
|
1422
|
+
self.conn.execute(f"SET lock_timeout = '{int(lock_timeout_ms)}ms'")
|
|
1423
|
+
for s in statements:
|
|
1424
|
+
self.conn.execute(self._ddl(s))
|
|
1425
|
+
self.conn.commit()
|
|
1426
|
+
for t in cache_tables:
|
|
1427
|
+
self._col_cache.pop(t, None)
|
|
1428
|
+
self._text_col_cache.pop(t, None)
|
|
1429
|
+
return
|
|
1430
|
+
except Exception as e:
|
|
1431
|
+
last = e
|
|
1432
|
+
self.conn.rollback()
|
|
1433
|
+
_time.sleep(backoff)
|
|
1434
|
+
raise RuntimeError(
|
|
1435
|
+
f"lock-contended DDL failed after {retries} attempts: {statements!r}: {last!r}"
|
|
1436
|
+
)
|
|
1437
|
+
finally:
|
|
1438
|
+
# lock_timeout is a session setting; restore the default so it
|
|
1439
|
+
# doesn't leak onto later queries on this (pooled) connection.
|
|
1440
|
+
try:
|
|
1441
|
+
self.conn.execute("RESET lock_timeout")
|
|
1442
|
+
self.conn.commit()
|
|
1443
|
+
except Exception:
|
|
1444
|
+
self.conn.rollback()
|
|
1445
|
+
|
|
1394
1446
|
def ensure_column(self, table: str, column: str, definition: str) -> None:
|
|
1395
|
-
"""Idempotently add a column if it's missing. Public helper
|
|
1396
|
-
migrations registered via api.services.register_migration
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1447
|
+
"""Idempotently add a column if it's missing, LOCK-SAFELY. Public helper
|
|
1448
|
+
for plugin migrations registered via api.services.register_migration.
|
|
1449
|
+
|
|
1450
|
+
On Postgres the ADD COLUMN is bounded by `lock_timeout` + retry so a
|
|
1451
|
+
migration can't hang the boot against a live old revision during a
|
|
1452
|
+
rolling deploy (see _alter_table_lock_safe). SQLite adds directly. The
|
|
1453
|
+
`definition` is the SQLite-flavoured column type (e.g. "TEXT",
|
|
1454
|
+
"INTEGER DEFAULT 0"); it is translated per dialect via _ddl."""
|
|
1455
|
+
if column in self._get_table_columns(table):
|
|
1456
|
+
return
|
|
1457
|
+
self._alter_table_lock_safe(
|
|
1458
|
+
[f'ALTER TABLE "{table}" ADD COLUMN {column} {definition}'], [table])
|
|
1459
|
+
|
|
1460
|
+
def drop_column(self, table: str, column: str) -> None:
|
|
1461
|
+
"""Idempotently drop a column if present, LOCK-SAFELY (same rolling-deploy
|
|
1462
|
+
protection as ensure_column). No-op if the column is already gone.
|
|
1463
|
+
Requires SQLite >= 3.35 for the SQLite backend (DROP COLUMN support)."""
|
|
1464
|
+
if column not in self._get_table_columns(table):
|
|
1465
|
+
return
|
|
1466
|
+
self._alter_table_lock_safe(
|
|
1467
|
+
[f'ALTER TABLE "{table}" DROP COLUMN "{column}"'], [table])
|
|
1401
1468
|
|
|
1402
1469
|
def _migrate(self):
|
|
1403
1470
|
"""Run each pending migration in order, tracking applied versions."""
|
|
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
|