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.
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/PKG-INFO +1 -1
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/chat.py +199 -2
- lambda_erp-0.8.24/api/routers/bank_reconciliation.py +100 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/mcp.py +19 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/frontend/src/api/client.ts +101 -0
- lambda_erp-0.8.24/lambda_erp/accounting/bank_reconciliation.py +685 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/bank_transaction.py +26 -30
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/journal_entry.py +15 -2
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/payment_entry.py +13 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/database.py +39 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/pyproject.toml +1 -1
- lambda_erp-0.8.22/api/routers/bank_reconciliation.py +0 -31
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/.gitignore +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/LICENSE +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/README.md +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/__init__.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/attachments.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/auth.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/bootstrap.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/demo_limits.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/deps.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/errors.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/list_values.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/main.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/oauth.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/pdf.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/providers.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/remarks_md.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/__init__.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/accounting.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/actions.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/admin.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/analytics.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/availability.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/bank_statements.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/documents.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/masters.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/proposals.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/reports.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/routers/setup.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/services.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/templates/document.html +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/api/templates/proposal.html +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/docs/agents/README.md +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/frontend/LICENSE +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/frontend/README.md +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/bank_account.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/bank_statement_import.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/camt.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/assets/__init__.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/assets/asset.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/assets/reservation.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/model.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.8.22 → lambda_erp-0.8.24}/terraform/README.md +0 -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.
|
|
2376
|
-
|
|
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 }>(
|