lambda-erp 0.8.22__tar.gz → 0.8.24__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 (96) hide show
  1. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/PKG-INFO +1 -1
  2. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/chat.py +199 -2
  3. lambda_erp-0.8.24/api/routers/bank_reconciliation.py +100 -0
  4. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/mcp.py +19 -0
  5. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/frontend/src/api/client.ts +101 -0
  6. lambda_erp-0.8.24/lambda_erp/accounting/bank_reconciliation.py +685 -0
  7. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/bank_transaction.py +26 -30
  8. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/journal_entry.py +15 -2
  9. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/payment_entry.py +13 -0
  10. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/database.py +39 -0
  11. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/pyproject.toml +1 -1
  12. lambda_erp-0.8.22/api/routers/bank_reconciliation.py +0 -31
  13. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/.gitignore +0 -0
  14. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/LICENSE +0 -0
  15. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/README.md +0 -0
  16. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/__init__.py +0 -0
  17. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/attachments.py +0 -0
  18. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/auth.py +0 -0
  19. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/bootstrap.py +0 -0
  20. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/demo_limits.py +0 -0
  21. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/deps.py +0 -0
  22. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/errors.py +0 -0
  23. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/list_values.py +0 -0
  24. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/main.py +0 -0
  25. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/oauth.py +0 -0
  26. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/pdf.py +0 -0
  27. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/providers.py +0 -0
  28. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/remarks_md.py +0 -0
  29. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/__init__.py +0 -0
  30. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/accounting.py +0 -0
  31. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/actions.py +0 -0
  32. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/admin.py +0 -0
  33. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/analytics.py +0 -0
  34. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/availability.py +0 -0
  35. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/bank_statements.py +0 -0
  36. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/chat_api.py +0 -0
  37. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/documents.py +0 -0
  38. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/masters.py +0 -0
  39. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/proposals.py +0 -0
  40. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/reports.py +0 -0
  41. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/setup.py +0 -0
  42. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/services.py +0 -0
  43. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/templates/document.html +0 -0
  44. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/templates/proposal.html +0 -0
  45. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/docs/agents/README.md +0 -0
  46. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/frontend/LICENSE +0 -0
  47. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/frontend/README.md +0 -0
  48. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/__init__.py +0 -0
  49. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/__init__.py +0 -0
  50. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/bank_account.py +0 -0
  51. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/bank_statement_import.py +0 -0
  52. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/budget.py +0 -0
  53. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/camt.py +0 -0
  54. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  55. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/general_ledger.py +0 -0
  56. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/pos_invoice.py +0 -0
  57. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/purchase_invoice.py +0 -0
  58. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/revaluation.py +0 -0
  59. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/sales_invoice.py +0 -0
  60. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/__init__.py +0 -0
  61. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/engine.py +0 -0
  62. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/pack.py +0 -0
  63. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
  64. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/packs/ch.py +0 -0
  65. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
  66. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
  67. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
  68. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/packs/generic.py +0 -0
  69. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/profiles.py +0 -0
  70. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/spine.py +0 -0
  71. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/subscription.py +0 -0
  72. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/assets/__init__.py +0 -0
  73. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/assets/asset.py +0 -0
  74. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/assets/reservation.py +0 -0
  75. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/buying/__init__.py +0 -0
  76. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/buying/purchase_order.py +0 -0
  77. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/controllers/__init__.py +0 -0
  78. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/controllers/currency.py +0 -0
  79. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/controllers/defaults.py +0 -0
  80. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/controllers/pricing_rule.py +0 -0
  81. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  82. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/exceptions.py +0 -0
  83. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/hooks.py +0 -0
  84. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/model.py +0 -0
  85. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/selling/__init__.py +0 -0
  86. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/selling/proposal.py +0 -0
  87. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/selling/quotation.py +0 -0
  88. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/selling/sales_order.py +0 -0
  89. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/simulation.py +0 -0
  90. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/stock/__init__.py +0 -0
  91. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/stock/delivery_note.py +0 -0
  92. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/stock/purchase_receipt.py +0 -0
  93. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/stock/stock_entry.py +0 -0
  94. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/stock/stock_ledger.py +0 -0
  95. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/utils.py +0 -0
  96. {lambda_erp-0.8.22 → lambda_erp-0.8.24}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lambda-erp
3
- Version: 0.8.22
3
+ Version: 0.8.24
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: Apache-2.0
@@ -866,6 +866,83 @@ TOOLS = [
866
866
  },
867
867
  },
868
868
  },
869
+ {
870
+ "type": "function",
871
+ "function": {
872
+ "name": "list_bank_reconciliation_queue",
873
+ "description": "List imported bank transactions awaiting reconciliation, or recently reconciled rows. Returns compact transaction details with masked counterparty IBANs. Requires manager access.",
874
+ "parameters": {
875
+ "type": "object",
876
+ "properties": {
877
+ "status": {"type": "string", "enum": ["Unreconciled", "Reconciled", "All"], "default": "Unreconciled"},
878
+ "limit": {"type": "integer", "minimum": 1, "maximum": 500, "default": 100},
879
+ },
880
+ },
881
+ },
882
+ },
883
+ {
884
+ "type": "function",
885
+ "function": {
886
+ "name": "suggest_bank_reconciliation",
887
+ "description": "Return deterministic candidates for one imported Bank Transaction: exact existing Payment/Journal vouchers and open invoice matches scored by amount, document reference, counterparty and date. This is read-only and must be called before proposing a reconciliation.",
888
+ "parameters": {
889
+ "type": "object",
890
+ "properties": {
891
+ "bank_transaction": {"type": "string"},
892
+ "limit": {"type": "integer", "minimum": 1, "maximum": 50, "default": 12},
893
+ },
894
+ "required": ["bank_transaction"],
895
+ },
896
+ },
897
+ },
898
+ {
899
+ "type": "function",
900
+ "function": {
901
+ "name": "reconcile_bank_transaction",
902
+ "description": "Reconcile one imported bank transaction after explicit user confirmation. mode=invoice_payment creates and submits a Payment Entry allocated to one or more invoices; mode=existing_voucher links an already-submitted Payment Entry or Journal Entry with an exactly matching bank leg; mode=journal creates and submits a two-sided Journal Entry against a non-bank, non-AR/AP account. This posts or links real accounting data. Never call in the same turn as the first proposal; confirmed must reflect a later explicit confirmation.",
903
+ "parameters": {
904
+ "type": "object",
905
+ "properties": {
906
+ "bank_transaction": {"type": "string"},
907
+ "mode": {"type": "string", "enum": ["invoice_payment", "existing_voucher", "journal"]},
908
+ "allocations": {
909
+ "type": "array",
910
+ "items": {
911
+ "type": "object",
912
+ "properties": {
913
+ "reference_doctype": {"type": "string", "enum": ["Sales Invoice", "Purchase Invoice"]},
914
+ "reference_name": {"type": "string"},
915
+ "allocated_amount": {"type": "number", "exclusiveMinimum": 0},
916
+ },
917
+ "required": ["reference_doctype", "reference_name", "allocated_amount"],
918
+ },
919
+ },
920
+ "voucher_type": {"type": "string", "enum": ["Payment Entry", "Journal Entry"]},
921
+ "voucher_no": {"type": "string"},
922
+ "counterparty_account": {"type": "string"},
923
+ "conversion_rate": {"type": "number", "exclusiveMinimum": 0},
924
+ "remarks": {"type": "string"},
925
+ "confirmed": {"type": "boolean"},
926
+ },
927
+ "required": ["bank_transaction", "mode", "confirmed"],
928
+ },
929
+ },
930
+ },
931
+ {
932
+ "type": "function",
933
+ "function": {
934
+ "name": "undo_bank_reconciliation",
935
+ "description": "Reverse an active bank reconciliation after explicit user confirmation. Vouchers created by reconciliation are cancelled and reversed; an existing linked voucher is only unlinked and remains submitted. Never call without explaining that distinction and receiving confirmation.",
936
+ "parameters": {
937
+ "type": "object",
938
+ "properties": {
939
+ "bank_transaction": {"type": "string"},
940
+ "confirmed": {"type": "boolean"},
941
+ },
942
+ "required": ["bank_transaction", "confirmed"],
943
+ },
944
+ },
945
+ },
869
946
  {
870
947
  "type": "function",
871
948
  "function": {
@@ -1958,6 +2035,97 @@ def _handle_import_bank_statement_attachments(args, user_info: dict | None = Non
1958
2035
  }
1959
2036
 
1960
2037
 
2038
+ def _bank_reconciliation_access(user_info: dict | None):
2039
+ if (user_info or {}).get("role") not in {"manager", "admin"}:
2040
+ return {"error": "Bank reconciliation requires manager access and is unavailable in demo mode."}
2041
+ return None
2042
+
2043
+
2044
+ def _handle_list_bank_reconciliation_queue(args, user_info: dict | None = None):
2045
+ denied = _bank_reconciliation_access(user_info)
2046
+ if denied:
2047
+ return denied
2048
+ from lambda_erp.accounting.bank_reconciliation import list_bank_transactions
2049
+ return list_bank_transactions(
2050
+ status=args.get("status") or "Unreconciled",
2051
+ limit=args.get("limit") or 100,
2052
+ )
2053
+
2054
+
2055
+ def _handle_suggest_bank_reconciliation(args, user_info: dict | None = None):
2056
+ denied = _bank_reconciliation_access(user_info)
2057
+ if denied:
2058
+ return denied
2059
+ from lambda_erp.accounting.bank_reconciliation import suggest_matches
2060
+ try:
2061
+ return suggest_matches(args.get("bank_transaction"), limit=args.get("limit") or 12)
2062
+ except Exception as exc:
2063
+ return {"error": str(exc)}
2064
+
2065
+
2066
+ def _handle_reconcile_bank_transaction(args, user_info: dict | None = None):
2067
+ denied = _bank_reconciliation_access(user_info)
2068
+ if denied:
2069
+ return denied
2070
+ if args.get("confirmed") is not True:
2071
+ return {
2072
+ "error": (
2073
+ "Explicit confirmation is required. Show the exact bank transaction, proposed "
2074
+ "voucher/allocations and any on-account remainder, then ask the user to confirm. "
2075
+ "Call this tool in a later turn with confirmed=true."
2076
+ )
2077
+ }
2078
+ from lambda_erp.accounting.bank_reconciliation import (
2079
+ reconcile_with_existing_voucher,
2080
+ reconcile_with_journal,
2081
+ reconcile_with_payment,
2082
+ )
2083
+ user = (user_info or {}).get("name")
2084
+ mode = args.get("mode")
2085
+ try:
2086
+ if mode == "invoice_payment":
2087
+ return reconcile_with_payment(
2088
+ args.get("bank_transaction"), args.get("allocations") or [],
2089
+ conversion_rate=args.get("conversion_rate"), user=user, confirmed=True,
2090
+ )
2091
+ if mode == "existing_voucher":
2092
+ return reconcile_with_existing_voucher(
2093
+ args.get("bank_transaction"), args.get("voucher_type"), args.get("voucher_no"),
2094
+ user=user, confirmed=True,
2095
+ )
2096
+ if mode == "journal":
2097
+ return reconcile_with_journal(
2098
+ args.get("bank_transaction"), args.get("counterparty_account"),
2099
+ conversion_rate=args.get("conversion_rate"), remarks=args.get("remarks"),
2100
+ user=user, confirmed=True,
2101
+ )
2102
+ return {"error": "mode must be invoice_payment, existing_voucher, or journal"}
2103
+ except Exception as exc:
2104
+ return {"error": str(exc)}
2105
+
2106
+
2107
+ def _handle_undo_bank_reconciliation(args, user_info: dict | None = None):
2108
+ denied = _bank_reconciliation_access(user_info)
2109
+ if denied:
2110
+ return denied
2111
+ if args.get("confirmed") is not True:
2112
+ return {
2113
+ "error": (
2114
+ "Explicit confirmation is required. Explain whether the linked voucher will be "
2115
+ "cancelled or only unlinked, then ask the user to confirm."
2116
+ )
2117
+ }
2118
+ from lambda_erp.accounting.bank_reconciliation import undo_reconciliation
2119
+ try:
2120
+ return undo_reconciliation(
2121
+ args.get("bank_transaction"),
2122
+ user=(user_info or {}).get("name"),
2123
+ confirmed=True,
2124
+ )
2125
+ except Exception as exc:
2126
+ return {"error": str(exc)}
2127
+
2128
+
1961
2129
  def _handle_query_dataset(args):
1962
2130
  from api.routers.analytics import aggregate_semantic_dataset
1963
2131
 
@@ -2117,6 +2285,10 @@ TOOL_HANDLERS = {
2117
2285
  "retrieve_chat_attachment": _handle_retrieve_chat_attachment,
2118
2286
  "preview_bank_statement_attachments": _handle_preview_bank_statement_attachments,
2119
2287
  "import_bank_statement_attachments": _handle_import_bank_statement_attachments,
2288
+ "list_bank_reconciliation_queue": _handle_list_bank_reconciliation_queue,
2289
+ "suggest_bank_reconciliation": _handle_suggest_bank_reconciliation,
2290
+ "reconcile_bank_transaction": _handle_reconcile_bank_transaction,
2291
+ "undo_bank_reconciliation": _handle_undo_bank_reconciliation,
2120
2292
  "query_dataset": _handle_query_dataset,
2121
2293
  "create_custom_analytics_report": lambda args: _handle_create_custom_analytics_report(args),
2122
2294
  "get_custom_analytics_report": lambda args: _handle_get_custom_analytics_report(args),
@@ -2372,8 +2544,21 @@ the conversation and never try to interpret the XML manually. Use this workflow:
2372
2544
 
2373
2545
  The CAMT import creates unposted Bank Transactions and their batch details only. It never creates,
2374
2546
  submits, or posts a Payment Entry, Journal Entry, or GL Entry. Do not imply that invoices have been
2375
- settled or the ledger changed merely because a statement was imported. Matching/reconciliation is a
2376
- separate later step. Keep Bank Account IBANs masked in ordinary chat responses.
2547
+ settled or the ledger changed merely because a statement was imported. Keep Bank Account IBANs
2548
+ masked in ordinary chat responses.
2549
+
2550
+ ### Bank reconciliation
2551
+ Use `list_bank_reconciliation_queue` and then `suggest_bank_reconciliation`; never guess from a raw
2552
+ description alone. Explain one exact proposal before doing anything. Reconciliation has three safe
2553
+ paths: (1) link an existing submitted Payment/Journal voucher whose bank leg matches exactly,
2554
+ (2) create and submit a Payment Entry allocated to one or more open invoices of the same party, or
2555
+ (3) create and submit a Journal Entry against a user-selected non-bank, non-AR/AP account. A created
2556
+ voucher changes the GL and may change invoice outstanding amounts. Ask for explicit confirmation in
2557
+ a later user turn, then call `reconcile_bank_transaction` with `confirmed=true`. Clearly disclose any
2558
+ unallocated on-account remainder. Never match a Bank Transaction directly to an invoice because that
2559
+ would omit the cash posting. For reversals, first explain whether the voucher was created by the
2560
+ reconciliation (it will be cancelled and reversed) or merely linked (it will only be unlinked), and
2561
+ then require explicit confirmation before `undo_bank_reconciliation`.
2377
2562
 
2378
2563
  ### Sales Cycle
2379
2564
  Quotation → Sales Order → Delivery Note (shipping) / Sales Invoice (billing) → Payment Entry
@@ -3213,6 +3398,18 @@ async def run_thinking_loop(
3213
3398
  tool_handlers["import_bank_statement_attachments"] = (
3214
3399
  lambda args: _handle_import_bank_statement_attachments(args, user_info)
3215
3400
  )
3401
+ tool_handlers["list_bank_reconciliation_queue"] = (
3402
+ lambda args: _handle_list_bank_reconciliation_queue(args, user_info)
3403
+ )
3404
+ tool_handlers["suggest_bank_reconciliation"] = (
3405
+ lambda args: _handle_suggest_bank_reconciliation(args, user_info)
3406
+ )
3407
+ tool_handlers["reconcile_bank_transaction"] = (
3408
+ lambda args: _handle_reconcile_bank_transaction(args, user_info)
3409
+ )
3410
+ tool_handlers["undo_bank_reconciliation"] = (
3411
+ lambda args: _handle_undo_bank_reconciliation(args, user_info)
3412
+ )
3216
3413
  _scoped_retrieve_attachment = (
3217
3414
  lambda args: _handle_retrieve_chat_attachment(args, session_id, user_id_for_tools)
3218
3415
  )
@@ -0,0 +1,100 @@
1
+ """Bank reconciliation queue, suggestions, posting, matching, and reversal."""
2
+
3
+ from fastapi import APIRouter, Depends, Query
4
+
5
+ from api.auth import require_role
6
+ from lambda_erp.accounting.bank_reconciliation import (
7
+ list_bank_transactions,
8
+ reconcile_with_existing_voucher,
9
+ reconcile_with_journal,
10
+ reconcile_with_payment,
11
+ suggest_matches,
12
+ undo_reconciliation,
13
+ )
14
+ from lambda_erp.accounting.subscription import Subscription
15
+
16
+
17
+ router = APIRouter(tags=["bank-reconciliation"], dependencies=[Depends(require_role("manager"))])
18
+
19
+
20
+ @router.get("/bank-reconciliation/transactions")
21
+ def transactions(
22
+ status: str = "Unreconciled",
23
+ limit: int = Query(default=100, ge=1, le=500),
24
+ ):
25
+ return list_bank_transactions(status=status, limit=limit)
26
+
27
+
28
+ @router.get("/bank-reconciliation/transactions/{bank_transaction}/suggestions")
29
+ def suggestions(
30
+ bank_transaction: str,
31
+ limit: int = Query(default=12, ge=1, le=50),
32
+ ):
33
+ return suggest_matches(bank_transaction, limit=limit)
34
+
35
+
36
+ @router.post("/bank-reconciliation/payment")
37
+ def post_payment(data: dict, user: dict = Depends(require_role("manager"))):
38
+ return reconcile_with_payment(
39
+ data.get("bank_transaction"),
40
+ data.get("allocations") or [],
41
+ conversion_rate=data.get("conversion_rate"),
42
+ user=user.get("name"),
43
+ confirmed=data.get("confirmed") is True,
44
+ )
45
+
46
+
47
+ @router.post("/bank-reconciliation/journal")
48
+ def post_journal(data: dict, user: dict = Depends(require_role("manager"))):
49
+ return reconcile_with_journal(
50
+ data.get("bank_transaction"),
51
+ data.get("counterparty_account"),
52
+ conversion_rate=data.get("conversion_rate"),
53
+ remarks=data.get("remarks"),
54
+ user=user.get("name"),
55
+ confirmed=data.get("confirmed") is True,
56
+ )
57
+
58
+
59
+ @router.post("/bank-reconciliation/match-existing")
60
+ def match_existing(data: dict, user: dict = Depends(require_role("manager"))):
61
+ return reconcile_with_existing_voucher(
62
+ data.get("bank_transaction"),
63
+ data.get("voucher_type"),
64
+ data.get("voucher_no"),
65
+ user=user.get("name"),
66
+ confirmed=data.get("confirmed") is True,
67
+ )
68
+
69
+
70
+ @router.post("/bank-reconciliation/undo")
71
+ def undo(data: dict, user: dict = Depends(require_role("manager"))):
72
+ return undo_reconciliation(
73
+ data.get("bank_transaction"),
74
+ user=user.get("name"),
75
+ confirmed=data.get("confirmed") is True,
76
+ )
77
+
78
+
79
+ # Compatibility for clients that used the old one-reference endpoint. It now
80
+ # performs the safe existing-voucher path and deliberately rejects invoices:
81
+ # an invoice alone is not a bank posting.
82
+ @router.post("/bank-reconciliation/match")
83
+ def match_transaction(data: dict, user: dict = Depends(require_role("manager"))):
84
+ return reconcile_with_existing_voucher(
85
+ data.get("bank_transaction"),
86
+ data.get("reference_doctype"),
87
+ data.get("reference_name"),
88
+ user=user.get("name"),
89
+ confirmed=data.get("confirmed") is True,
90
+ )
91
+
92
+
93
+ @router.post("/documents/subscription/{name}/process")
94
+ def process_subscription(name: str):
95
+ """Process a subscription to generate the next invoice if due."""
96
+ sub = Subscription.load(name)
97
+ result = sub.process()
98
+ if result:
99
+ return {"status": "invoice_created", "invoice": result}
100
+ return {"status": "no_invoice_due"}
@@ -48,8 +48,13 @@ _WRITE = {
48
48
  "create_document", "update_document", "batch_update_documents",
49
49
  "submit_document", "cancel_document",
50
50
  "discard_document", "convert_document", "create_master", "update_master",
51
+ "reconcile_bank_transaction", "undo_bank_reconciliation",
51
52
  }
52
53
  _ADMIN = {"delete_master"}
54
+ _MANAGER_ONLY = {
55
+ "list_bank_reconciliation_queue", "suggest_bank_reconciliation",
56
+ "reconcile_bank_transaction", "undo_bank_reconciliation",
57
+ }
53
58
 
54
59
 
55
60
  def _can_write(role) -> bool:
@@ -63,6 +68,8 @@ def _allowed(name: str, role) -> bool:
63
68
  return services.registered_action_allowed(name, role)
64
69
  if name in _ADMIN:
65
70
  return role == "admin"
71
+ if name in _MANAGER_ONLY:
72
+ return role in ("manager", "admin")
66
73
  if name in _WRITE:
67
74
  return _can_write(role)
68
75
  return True
@@ -99,6 +106,18 @@ def _call(name: str, args: dict, user: dict):
99
106
  # delete_master needs the caller's role (admin-only); handled by the chat's
100
107
  # role-aware variant.
101
108
  handlers["delete_master"] = lambda a: chat_mod._handle_delete_master(a, user)
109
+ handlers["list_bank_reconciliation_queue"] = (
110
+ lambda a: chat_mod._handle_list_bank_reconciliation_queue(a, user)
111
+ )
112
+ handlers["suggest_bank_reconciliation"] = (
113
+ lambda a: chat_mod._handle_suggest_bank_reconciliation(a, user)
114
+ )
115
+ handlers["reconcile_bank_transaction"] = (
116
+ lambda a: chat_mod._handle_reconcile_bank_transaction(a, user)
117
+ )
118
+ handlers["undo_bank_reconciliation"] = (
119
+ lambda a: chat_mod._handle_undo_bank_reconciliation(a, user)
120
+ )
102
121
  handlers.update(services.registered_action_handlers(user))
103
122
  handler = handlers.get(name)
104
123
  if handler is None:
@@ -126,6 +126,69 @@ export interface BankStatementImportResult {
126
126
  exact_duplicate_imports: string[];
127
127
  }
128
128
 
129
+ export interface BankReconciliationTransaction {
130
+ name: string;
131
+ bank_account: string;
132
+ bank_account_id: string;
133
+ bank_statement_import: string;
134
+ company: string;
135
+ base_currency: string;
136
+ bank_currency: string;
137
+ posting_date: string;
138
+ value_date: string | null;
139
+ deposit: number;
140
+ withdrawal: number;
141
+ amount: number;
142
+ currency: string;
143
+ description: string | null;
144
+ remittance_information: string | null;
145
+ reference_number: string | null;
146
+ counterparty_name: string | null;
147
+ counterparty_iban_masked: string;
148
+ structured_reference: string | null;
149
+ status: string;
150
+ reference_doctype: string | null;
151
+ reference_name: string | null;
152
+ reconciled_by: string | null;
153
+ reconciled_at: string | null;
154
+ }
155
+
156
+ export interface BankInvoiceSuggestion {
157
+ kind: "invoice";
158
+ reference_doctype: "Sales Invoice" | "Purchase Invoice";
159
+ reference_name: string;
160
+ party_type: "Customer" | "Supplier";
161
+ party: string;
162
+ party_name: string;
163
+ payment_type: "Receive" | "Pay";
164
+ posting_date: string;
165
+ due_date: string | null;
166
+ currency: string;
167
+ grand_total: number;
168
+ outstanding_amount: number;
169
+ suggested_allocation: number;
170
+ score: number;
171
+ reasons: string[];
172
+ }
173
+
174
+ export interface BankVoucherSuggestion {
175
+ kind: "existing_voucher";
176
+ voucher_type: "Payment Entry" | "Journal Entry";
177
+ voucher_no: string;
178
+ posting_date: string;
179
+ amount: number;
180
+ currency: string;
181
+ score: number;
182
+ reasons: string[];
183
+ }
184
+
185
+ export interface BankReconciliationSuggestions {
186
+ transaction: BankReconciliationTransaction;
187
+ active_reconciliation: Record<string, any> | null;
188
+ existing_vouchers: BankVoucherSuggestion[];
189
+ invoices: BankInvoiceSuggestion[];
190
+ }
191
+
129
192
  function qs(params?: Record<string, string | number | undefined>) {
130
193
  if (!params) return "";
131
194
  const clean = Object.fromEntries(
@@ -237,6 +300,44 @@ export const api = {
237
300
  bankStatementSourceUrl: (name: string) =>
238
301
  `${BASE}/bank-statements/${encodeURIComponent(name)}/source`,
239
302
 
303
+ listBankReconciliationTransactions: (status = "Unreconciled", limit = 100) =>
304
+ request<{ rows: BankReconciliationTransaction[] }>(
305
+ `/bank-reconciliation/transactions${qs({ status, limit })}`,
306
+ ),
307
+
308
+ getBankReconciliationSuggestions: (name: string, limit = 12) =>
309
+ request<BankReconciliationSuggestions>(
310
+ `/bank-reconciliation/transactions/${encodeURIComponent(name)}/suggestions${qs({ limit })}`,
311
+ ),
312
+
313
+ reconcileBankPayment: (data: {
314
+ bank_transaction: string;
315
+ allocations: Array<{ reference_doctype: string; reference_name: string; allocated_amount: number }>;
316
+ conversion_rate?: number;
317
+ confirmed: boolean;
318
+ }) => request<any>("/bank-reconciliation/payment", { method: "POST", body: JSON.stringify(data) }),
319
+
320
+ reconcileBankJournal: (data: {
321
+ bank_transaction: string;
322
+ counterparty_account: string;
323
+ conversion_rate?: number;
324
+ remarks?: string;
325
+ confirmed: boolean;
326
+ }) => request<any>("/bank-reconciliation/journal", { method: "POST", body: JSON.stringify(data) }),
327
+
328
+ reconcileBankExistingVoucher: (data: {
329
+ bank_transaction: string;
330
+ voucher_type: string;
331
+ voucher_no: string;
332
+ confirmed: boolean;
333
+ }) => request<any>("/bank-reconciliation/match-existing", { method: "POST", body: JSON.stringify(data) }),
334
+
335
+ undoBankReconciliation: (bankTransaction: string) =>
336
+ request<any>("/bank-reconciliation/undo", {
337
+ method: "POST",
338
+ body: JSON.stringify({ bank_transaction: bankTransaction, confirmed: true }),
339
+ }),
340
+
240
341
  // Chat
241
342
  createChatSession: () =>
242
343
  request<{ id: string; title: string; created_at: string; updated_at: string; last_message_at?: string | null }>(