lambda-erp 0.8.24__tar.gz → 0.8.25__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 (95) hide show
  1. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/PKG-INFO +1 -1
  2. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/bank_reconciliation.py +40 -26
  3. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/database.py +29 -7
  4. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/pyproject.toml +1 -1
  5. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/.gitignore +0 -0
  6. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/LICENSE +0 -0
  7. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/README.md +0 -0
  8. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/__init__.py +0 -0
  9. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/attachments.py +0 -0
  10. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/auth.py +0 -0
  11. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/bootstrap.py +0 -0
  12. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/chat.py +0 -0
  13. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/demo_limits.py +0 -0
  14. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/deps.py +0 -0
  15. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/errors.py +0 -0
  16. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/list_values.py +0 -0
  17. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/main.py +0 -0
  18. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/oauth.py +0 -0
  19. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/pdf.py +0 -0
  20. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/providers.py +0 -0
  21. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/remarks_md.py +0 -0
  22. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/routers/__init__.py +0 -0
  23. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/routers/accounting.py +0 -0
  24. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/routers/actions.py +0 -0
  25. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/routers/admin.py +0 -0
  26. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/routers/analytics.py +0 -0
  27. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/routers/availability.py +0 -0
  28. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/routers/bank_reconciliation.py +0 -0
  29. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/routers/bank_statements.py +0 -0
  30. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/routers/chat_api.py +0 -0
  31. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/routers/documents.py +0 -0
  32. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/routers/masters.py +0 -0
  33. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/routers/mcp.py +0 -0
  34. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/routers/proposals.py +0 -0
  35. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/routers/reports.py +0 -0
  36. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/routers/setup.py +0 -0
  37. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/services.py +0 -0
  38. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/templates/document.html +0 -0
  39. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/api/templates/proposal.html +0 -0
  40. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/docs/agents/README.md +0 -0
  41. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/frontend/LICENSE +0 -0
  42. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/frontend/README.md +0 -0
  43. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/frontend/src/api/client.ts +0 -0
  44. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/__init__.py +0 -0
  45. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/__init__.py +0 -0
  46. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/bank_account.py +0 -0
  47. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/bank_statement_import.py +0 -0
  48. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/bank_transaction.py +0 -0
  49. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/budget.py +0 -0
  50. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/camt.py +0 -0
  51. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  52. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/general_ledger.py +0 -0
  53. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/journal_entry.py +0 -0
  54. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/payment_entry.py +0 -0
  55. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/pos_invoice.py +0 -0
  56. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/purchase_invoice.py +0 -0
  57. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/revaluation.py +0 -0
  58. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/sales_invoice.py +0 -0
  59. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/setup/__init__.py +0 -0
  60. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/setup/engine.py +0 -0
  61. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/setup/pack.py +0 -0
  62. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
  63. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/setup/packs/ch.py +0 -0
  64. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
  65. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
  66. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
  67. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/setup/packs/generic.py +0 -0
  68. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/setup/profiles.py +0 -0
  69. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/setup/spine.py +0 -0
  70. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/accounting/subscription.py +0 -0
  71. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/assets/__init__.py +0 -0
  72. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/assets/asset.py +0 -0
  73. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/assets/reservation.py +0 -0
  74. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/buying/__init__.py +0 -0
  75. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/buying/purchase_order.py +0 -0
  76. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/controllers/__init__.py +0 -0
  77. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/controllers/currency.py +0 -0
  78. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/controllers/defaults.py +0 -0
  79. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/controllers/pricing_rule.py +0 -0
  80. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  81. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/exceptions.py +0 -0
  82. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/hooks.py +0 -0
  83. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/model.py +0 -0
  84. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/selling/__init__.py +0 -0
  85. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/selling/proposal.py +0 -0
  86. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/selling/quotation.py +0 -0
  87. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/selling/sales_order.py +0 -0
  88. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/simulation.py +0 -0
  89. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/stock/__init__.py +0 -0
  90. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/stock/delivery_note.py +0 -0
  91. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/stock/purchase_receipt.py +0 -0
  92. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/stock/stock_entry.py +0 -0
  93. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/stock/stock_ledger.py +0 -0
  94. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/lambda_erp/utils.py +0 -0
  95. {lambda_erp-0.8.24 → lambda_erp-0.8.25}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lambda-erp
3
- Version: 0.8.24
3
+ Version: 0.8.25
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: Apache-2.0
@@ -256,10 +256,10 @@ def _voucher_candidates(tx: dict, limit: int) -> list[dict]:
256
256
  'AND gle.voucher_type IN (?, ?) '
257
257
  'AND NOT EXISTS (SELECT 1 FROM "Bank Reconciliation" br '
258
258
  ' WHERE br.voucher_type = gle.voucher_type AND br.voucher_no = gle.voucher_no '
259
- ' AND br.status = ?) '
259
+ ' AND br.status = ? AND br.bank_account = ?) '
260
260
  'GROUP BY gle.voucher_type, gle.voucher_no '
261
261
  'ORDER BY MIN(gle.posting_date) DESC LIMIT 500',
262
- [tx["bank_account"], "Payment Entry", "Journal Entry", "Active"],
262
+ [tx["bank_account"], "Payment Entry", "Journal Entry", "Active", tx["bank_account"]],
263
263
  )
264
264
  expected = _amount(tx)
265
265
  deposit = _is_deposit(tx)
@@ -311,6 +311,7 @@ def _new_audit(tx: dict, *, mode: str, voucher_type: str, voucher_no: str,
311
311
  get_db().insert("Bank Reconciliation", {
312
312
  "name": name,
313
313
  "bank_transaction": tx["name"],
314
+ "bank_account": tx["bank_account"],
314
315
  "mode": mode,
315
316
  "voucher_type": voucher_type,
316
317
  "voucher_no": voucher_no,
@@ -360,34 +361,35 @@ def reverse_generated_reconciliation(reconciliation: str | None, *, voucher_type
360
361
  "Bank Reconciliation", reconciliation,
361
362
  ["name", "bank_transaction", "voucher_type", "voucher_no", "status", "reversed_by"],
362
363
  )
364
+ rows = [row] if row else []
363
365
  else:
364
- matches = db.sql(
366
+ rows = db.sql(
365
367
  'SELECT name, bank_transaction, voucher_type, voucher_no, status, reversed_by '
366
368
  'FROM "Bank Reconciliation" WHERE voucher_type = ? AND voucher_no = ? '
367
- 'AND status = ? LIMIT 1',
369
+ 'AND status = ?',
368
370
  [voucher_type, voucher_no, "Active"],
369
371
  )
370
- row = matches[0] if matches else None
371
- if not row or row.get("status") != "Active":
372
- return
373
- if row.get("voucher_type") != voucher_type or row.get("voucher_no") != voucher_no:
374
- raise ValidationError("Bank reconciliation voucher does not match the cancelled document")
375
- tx = _transaction(row["bank_transaction"])
376
- db.set_value("Bank Transaction", tx["name"], {
377
- "reference_doctype": None,
378
- "reference_name": None,
379
- "allocated_amount": 0,
380
- "unallocated_amount": _amount(tx),
381
- "status": "Unreconciled",
382
- "reconciled_by": None,
383
- "reconciled_at": None,
384
- "modified": now(),
385
- })
386
- db.set_value("Bank Reconciliation", row["name"], {
387
- "status": "Reversed",
388
- "reversed_by": row.get("reversed_by"),
389
- "reversed_at": now(),
390
- })
372
+ for row in rows:
373
+ if row.get("status") != "Active":
374
+ continue
375
+ if row.get("voucher_type") != voucher_type or row.get("voucher_no") != voucher_no:
376
+ raise ValidationError("Bank reconciliation voucher does not match the cancelled document")
377
+ tx = _transaction(row["bank_transaction"])
378
+ db.set_value("Bank Transaction", tx["name"], {
379
+ "reference_doctype": None,
380
+ "reference_name": None,
381
+ "allocated_amount": 0,
382
+ "unallocated_amount": _amount(tx),
383
+ "status": "Unreconciled",
384
+ "reconciled_by": None,
385
+ "reconciled_at": None,
386
+ "modified": now(),
387
+ })
388
+ db.set_value("Bank Reconciliation", row["name"], {
389
+ "status": "Reversed",
390
+ "reversed_by": row.get("reversed_by"),
391
+ "reversed_at": now(),
392
+ })
391
393
 
392
394
 
393
395
  def _allocation_profile(tx: dict, allocations: list[dict]) -> tuple[tuple[str, str, str, str], list[dict]]:
@@ -586,7 +588,19 @@ def _validate_existing_voucher(tx: dict, voucher_type: str, voucher_no: str) ->
586
588
  raise ValidationError(f"{voucher_type} {voucher_no} is unavailable or not submitted")
587
589
  if document.get("company") != tx["company"]:
588
590
  raise ValidationError("Voucher and bank transaction belong to different companies")
589
- rows = get_db().sql(
591
+ db = get_db()
592
+ used = db.sql(
593
+ 'SELECT bank_transaction FROM "Bank Reconciliation" '
594
+ 'WHERE voucher_type = ? AND voucher_no = ? AND bank_account = ? '
595
+ 'AND status = ? LIMIT 1',
596
+ [voucher_type, voucher_no, tx["bank_account"], "Active"],
597
+ )
598
+ if used:
599
+ raise ValidationError(
600
+ f"The {voucher_type} {voucher_no} bank movement on account "
601
+ f"{tx['bank_account']} is already reconciled with {used[0]['bank_transaction']}"
602
+ )
603
+ rows = db.sql(
590
604
  'SELECT COALESCE(SUM(debit), 0) AS debit, COALESCE(SUM(credit), 0) AS credit, '
591
605
  'COALESCE(SUM(debit_in_account_currency), 0) AS debit_ccy, '
592
606
  'COALESCE(SUM(credit_in_account_currency), 0) AS credit_ccy '
@@ -1423,12 +1423,14 @@ class Database:
1423
1423
  """CREATE INDEX IF NOT EXISTS "ix_bank_transaction_detail_parent"
1424
1424
  ON "Bank Transaction Detail" (parent, idx)""",
1425
1425
 
1426
- # Immutable-ish audit trail for bank reconciliation decisions. A
1427
- # transaction can have only one active reconciliation, while old
1428
- # reversed rows remain available for review.
1426
+ # Audit trail for bank reconciliation decisions. A transaction can
1427
+ # have only one active reconciliation. A multi-bank Journal Entry
1428
+ # may be linked once per bank account; reversed rows remain for
1429
+ # review.
1429
1430
  """CREATE TABLE IF NOT EXISTS "Bank Reconciliation" (
1430
1431
  name TEXT PRIMARY KEY,
1431
1432
  bank_transaction TEXT NOT NULL,
1433
+ bank_account TEXT NOT NULL,
1432
1434
  mode TEXT NOT NULL,
1433
1435
  voucher_type TEXT NOT NULL,
1434
1436
  voucher_no TEXT NOT NULL,
@@ -1438,15 +1440,13 @@ class Database:
1438
1440
  created_at TEXT,
1439
1441
  reversed_by TEXT,
1440
1442
  reversed_at TEXT,
1441
- FOREIGN KEY (bank_transaction) REFERENCES "Bank Transaction"(name)
1443
+ FOREIGN KEY (bank_transaction) REFERENCES "Bank Transaction"(name),
1444
+ FOREIGN KEY (bank_account) REFERENCES "Account"(name)
1442
1445
  )""",
1443
1446
 
1444
1447
  """CREATE UNIQUE INDEX IF NOT EXISTS "ux_bank_reconciliation_active_transaction"
1445
1448
  ON "Bank Reconciliation" (bank_transaction) WHERE status = 'Active'""",
1446
1449
 
1447
- """CREATE UNIQUE INDEX IF NOT EXISTS "ux_bank_reconciliation_active_voucher"
1448
- ON "Bank Reconciliation" (voucher_type, voucher_no) WHERE status = 'Active'""",
1449
-
1450
1450
  # --- Chat Sessions ---
1451
1451
  """CREATE TABLE IF NOT EXISTS "Chat Session" (
1452
1452
  id TEXT PRIMARY KEY,
@@ -2427,6 +2427,27 @@ def _m024_bank_reconciliation(db: "Database") -> None:
2427
2427
  db._add_column_if_missing("Bank Transaction", "reconciled_at", "TEXT")
2428
2428
 
2429
2429
 
2430
+ def _m025_bank_reconciliation_multi_account_voucher(db: "Database") -> None:
2431
+ """Allow one voucher to reconcile one bank movement per bank account.
2432
+
2433
+ Version 24 allowed only one active reconciliation for an entire voucher.
2434
+ That rejects legitimate multi-bank journals (for example one CHF fee and
2435
+ one USD fee collected in a single Journal Entry). Keep the conservative
2436
+ one-use rule at the bank-account leg instead.
2437
+ """
2438
+ db.ensure_column("Bank Reconciliation", "bank_account", "TEXT")
2439
+ db._alter_table_lock_safe([
2440
+ 'DROP INDEX IF EXISTS "ux_bank_reconciliation_active_voucher"',
2441
+ 'UPDATE "Bank Reconciliation" SET bank_account = ('
2442
+ 'SELECT bt.bank_account FROM "Bank Transaction" bt '
2443
+ 'WHERE bt.name = "Bank Reconciliation".bank_transaction'
2444
+ ') WHERE bank_account IS NULL',
2445
+ 'CREATE UNIQUE INDEX IF NOT EXISTS "ux_bank_reconciliation_active_voucher_account" '
2446
+ 'ON "Bank Reconciliation" (voucher_type, voucher_no, bank_account) '
2447
+ "WHERE status = 'Active'",
2448
+ ], ["Bank Reconciliation"])
2449
+
2450
+
2430
2451
  Database.MIGRATIONS = [
2431
2452
  (1, "chat_message_session_id", _m001_chat_message_session_id),
2432
2453
  (2, "chat_session_user_id", _m002_chat_session_user_id),
@@ -2452,6 +2473,7 @@ Database.MIGRATIONS = [
2452
2473
  (22, "company_default_tax_templates", _m022_company_default_tax_templates),
2453
2474
  (23, "camt_bank_statement_import", _m023_camt_bank_statement_import),
2454
2475
  (24, "bank_reconciliation", _m024_bank_reconciliation),
2476
+ (25, "bank_reconciliation_multi_account_voucher", _m025_bank_reconciliation_multi_account_voucher),
2455
2477
  ]
2456
2478
 
2457
2479
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.8.24"
3
+ version = "0.8.25"
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