lambda-erp 0.8.20__tar.gz → 0.8.22__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.20 → lambda_erp-0.8.22}/PKG-INFO +6 -5
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/README.md +4 -4
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/attachments.py +20 -2
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/auth.py +1 -1
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/chat.py +261 -70
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/main.py +2 -1
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/routers/availability.py +3 -2
- lambda_erp-0.8.22/api/routers/bank_statements.py +118 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/routers/chat_api.py +1 -1
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/routers/masters.py +41 -35
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/services.py +164 -11
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/frontend/src/api/client.ts +85 -1
- lambda_erp-0.8.22/lambda_erp/accounting/bank_account.py +77 -0
- lambda_erp-0.8.22/lambda_erp/accounting/bank_statement_import.py +417 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/bank_transaction.py +22 -3
- lambda_erp-0.8.22/lambda_erp/accounting/camt.py +457 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/revaluation.py +1 -1
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/database.py +177 -3
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/pyproject.toml +2 -1
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/.gitignore +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/LICENSE +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/__init__.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/bootstrap.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/demo_limits.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/deps.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/errors.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/list_values.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/oauth.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/pdf.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/providers.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/remarks_md.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/routers/__init__.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/routers/accounting.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/routers/actions.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/routers/admin.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/routers/analytics.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/routers/documents.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/routers/mcp.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/routers/proposals.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/routers/reports.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/routers/setup.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/templates/document.html +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/api/templates/proposal.html +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/docs/agents/README.md +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/frontend/LICENSE +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/frontend/README.md +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/assets/__init__.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/assets/asset.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/assets/reservation.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/model.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.8.20 → lambda_erp-0.8.22}/terraform/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lambda-erp
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.22
|
|
4
4
|
Summary: Core ERP logic - accounting, sales, purchasing, inventory
|
|
5
5
|
Author: TORUS INVESTMENTS AG
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -8,6 +8,7 @@ License-File: LICENSE
|
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
9
|
Requires-Dist: anthropic>=0.40
|
|
10
10
|
Requires-Dist: bcrypt>=5
|
|
11
|
+
Requires-Dist: defusedxml>=0.7
|
|
11
12
|
Requires-Dist: fastapi>=0.115
|
|
12
13
|
Requires-Dist: holidays>=0.40
|
|
13
14
|
Requires-Dist: httpx>=0.27
|
|
@@ -92,7 +93,7 @@ Each of those is hours of skilled work today. With a capable LLM in the loop, th
|
|
|
92
93
|
▼
|
|
93
94
|
┌──────────────────────────────────────────┐
|
|
94
95
|
│ LLM orchestrator │
|
|
95
|
-
│ - GPT-5.
|
|
96
|
+
│ - GPT-5.6 drives the reasoning loop │
|
|
96
97
|
│ - Tool-use: document CRUD, search, │
|
|
97
98
|
│ reports, aggregations, analytics │
|
|
98
99
|
│ - Delegates JS generation to Anthropic │
|
|
@@ -449,9 +450,9 @@ replacement file.
|
|
|
449
450
|
Both the backend seams (document classes, lifecycle hooks, converters, plugin
|
|
450
451
|
loading) and the frontend seams (doctype/route/nav/component registries,
|
|
451
452
|
branding, configurable API base, Tailwind preset) are implemented. The backend
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
for the
|
|
453
|
+
ships as a pip package and the frontend as the
|
|
454
|
+
`@lambda-development/erp-core` npm package. See
|
|
455
|
+
[`docs/releasing.md`](docs/releasing.md) for the release process.
|
|
455
456
|
|
|
456
457
|
---
|
|
457
458
|
|
|
@@ -64,7 +64,7 @@ Each of those is hours of skilled work today. With a capable LLM in the loop, th
|
|
|
64
64
|
▼
|
|
65
65
|
┌──────────────────────────────────────────┐
|
|
66
66
|
│ LLM orchestrator │
|
|
67
|
-
│ - GPT-5.
|
|
67
|
+
│ - GPT-5.6 drives the reasoning loop │
|
|
68
68
|
│ - Tool-use: document CRUD, search, │
|
|
69
69
|
│ reports, aggregations, analytics │
|
|
70
70
|
│ - Delegates JS generation to Anthropic │
|
|
@@ -421,9 +421,9 @@ replacement file.
|
|
|
421
421
|
Both the backend seams (document classes, lifecycle hooks, converters, plugin
|
|
422
422
|
loading) and the frontend seams (doctype/route/nav/component registries,
|
|
423
423
|
branding, configurable API base, Tailwind preset) are implemented. The backend
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
for the
|
|
424
|
+
ships as a pip package and the frontend as the
|
|
425
|
+
`@lambda-development/erp-core` npm package. See
|
|
426
|
+
[`docs/releasing.md`](docs/releasing.md) for the release process.
|
|
427
427
|
|
|
428
428
|
---
|
|
429
429
|
|
|
@@ -49,10 +49,17 @@ OFFICE_MIME_TYPES = {
|
|
|
49
49
|
# Plain-text formats — inlined into the prompt as text (no file part needed).
|
|
50
50
|
TEXT_MIME_TYPES = {"text/csv", "text/plain"}
|
|
51
51
|
|
|
52
|
+
# Structured bank uploads are retained as ordinary chat attachments, but are
|
|
53
|
+
# never dumped wholesale into the LLM context. Dedicated deterministic tools
|
|
54
|
+
# parse them and return a compact preview instead.
|
|
55
|
+
STRUCTURED_TOOL_MIME_TYPES = {
|
|
56
|
+
"application/xml", "text/xml", "application/zip", "application/x-zip-compressed",
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
ALLOWED_MIME_TYPES = {
|
|
53
60
|
"image/png", "image/jpeg", "image/gif", "image/webp",
|
|
54
61
|
"application/pdf",
|
|
55
|
-
} | OFFICE_MIME_TYPES | TEXT_MIME_TYPES
|
|
62
|
+
} | OFFICE_MIME_TYPES | TEXT_MIME_TYPES | STRUCTURED_TOOL_MIME_TYPES
|
|
56
63
|
|
|
57
64
|
# Extension -> canonical mime, so a correctly-named file still uploads when the
|
|
58
65
|
# browser/OS mislabels its type (Office files are frequently sent as
|
|
@@ -69,6 +76,8 @@ _EXT_TO_MIME = {
|
|
|
69
76
|
"odp": "application/vnd.oasis.opendocument.presentation",
|
|
70
77
|
"csv": "text/csv",
|
|
71
78
|
"txt": "text/plain",
|
|
79
|
+
"xml": "application/xml",
|
|
80
|
+
"zip": "application/zip",
|
|
72
81
|
"pdf": "application/pdf",
|
|
73
82
|
"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg",
|
|
74
83
|
"gif": "image/gif", "webp": "image/webp",
|
|
@@ -240,7 +249,8 @@ async def upload_attachment(
|
|
|
240
249
|
raise HTTPException(
|
|
241
250
|
status_code=400,
|
|
242
251
|
detail=(f"Unsupported file type: {mime or ext or 'unknown'}. Allowed: images, PDF, "
|
|
243
|
-
"spreadsheets (Excel/CSV/ODS),
|
|
252
|
+
"spreadsheets (Excel/CSV/ODS), documents (Word/OpenDocument), "
|
|
253
|
+
"and structured bank files (XML/ZIP)."),
|
|
244
254
|
)
|
|
245
255
|
|
|
246
256
|
data = await file.read()
|
|
@@ -389,6 +399,14 @@ def build_multimodal_content(attachment: dict) -> dict:
|
|
|
389
399
|
"type": "image_url",
|
|
390
400
|
"image_url": {"url": f"data:{mime};base64,{data_b64}"},
|
|
391
401
|
}
|
|
402
|
+
if mime in STRUCTURED_TOOL_MIME_TYPES:
|
|
403
|
+
return {
|
|
404
|
+
"type": "text",
|
|
405
|
+
"text": (
|
|
406
|
+
f"[Structured attachment: {filename}. Do not retrieve or parse its raw contents in "
|
|
407
|
+
"the conversation; use the dedicated bank-statement preview tool.]"
|
|
408
|
+
),
|
|
409
|
+
}
|
|
392
410
|
if mime in TEXT_MIME_TYPES or mime.startswith("text/"):
|
|
393
411
|
text = attachment["data"].decode("utf-8", errors="replace")
|
|
394
412
|
return {"type": "text", "text": f"[File: {filename}]\n{text}"}
|
|
@@ -612,7 +612,7 @@ DEFAULTS = {
|
|
|
612
612
|
"allow_public_signup": "0",
|
|
613
613
|
# When "1", the programmatic chat API (POST /api/v1/chat, Bearer API keys) is
|
|
614
614
|
# active. Default off — an admin turns it on and issues keys. See
|
|
615
|
-
# docs/chat-api
|
|
615
|
+
# docs/chat-api.md.
|
|
616
616
|
"chat_api_enabled": "0",
|
|
617
617
|
# When "1", the same Bearer API keys authenticate the regular REST API
|
|
618
618
|
# (/api/documents, /api/masters, /api/reports, …) in addition to the session
|
|
@@ -448,7 +448,7 @@ DOCUMENT_SLUGS = [
|
|
|
448
448
|
"purchase-order", "purchase-invoice",
|
|
449
449
|
"payment-entry", "journal-entry", "stock-entry",
|
|
450
450
|
"delivery-note", "purchase-receipt", "pos-invoice",
|
|
451
|
-
"pricing-rule", "budget", "subscription", "bank-transaction",
|
|
451
|
+
"pricing-rule", "budget", "subscription", "bank-account", "bank-transaction",
|
|
452
452
|
# Owned equipment and its hire calendar. Core doctypes, so they belong in
|
|
453
453
|
# this list rather than surfacing as "deployment-specific extensions"
|
|
454
454
|
# (_extra_document_slugs is defined as everything NOT named here).
|
|
@@ -478,6 +478,20 @@ TOOLS = [
|
|
|
478
478
|
},
|
|
479
479
|
},
|
|
480
480
|
},
|
|
481
|
+
{
|
|
482
|
+
"type": "function",
|
|
483
|
+
"function": {
|
|
484
|
+
"name": "get_document_fields",
|
|
485
|
+
"description": "List the available columns of a document type and identify which are text fields. Call this before building list_documents filters when you are unsure of a field name or whether it supports the case-insensitive contains operator. Also returns the default fields used by free-text search.",
|
|
486
|
+
"parameters": {
|
|
487
|
+
"type": "object",
|
|
488
|
+
"properties": {
|
|
489
|
+
"doctype": {"type": "string", "enum": DOCUMENT_SLUGS},
|
|
490
|
+
},
|
|
491
|
+
"required": ["doctype"],
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
},
|
|
481
495
|
{
|
|
482
496
|
"type": "function",
|
|
483
497
|
"function": {
|
|
@@ -627,7 +641,7 @@ TOOLS = [
|
|
|
627
641
|
"type": "function",
|
|
628
642
|
"function": {
|
|
629
643
|
"name": "search_masters",
|
|
630
|
-
"description": "Search master data (customers, suppliers, items, warehouses, accounts, companies, cost centers).
|
|
644
|
+
"description": "Search and filter master data (customers, suppliers, items, warehouses, accounts, companies, cost centers). `query` is a case-insensitive free-text lookup with fuzzy fallback for simple misspellings. Use `filters` for deterministic field-aware queries and for combining different values across fields. Returns ALL matching records by default (no cap); pass `limit` only to bound a large list.",
|
|
631
645
|
"parameters": {
|
|
632
646
|
"type": "object",
|
|
633
647
|
"properties": {
|
|
@@ -638,7 +652,20 @@ TOOLS = [
|
|
|
638
652
|
"items": {"type": "string"},
|
|
639
653
|
"description": "Recommended: the column(s) to search, e.g. [\"city\"] or [\"customer_name\"]. Narrowing here is faster and avoids matching unrelated columns. Omit only when you genuinely don't know which field holds the value, to search all standard text fields.",
|
|
640
654
|
},
|
|
655
|
+
"filters": {
|
|
656
|
+
"type": "object",
|
|
657
|
+
"description": "Optional deterministic filters on any real column, ANDed together and with `query`. Scalar values are exact: {\"disabled\": 0}. Text substring: {\"legal_form\": [\"contains\", \"AG\"]}. Comparisons and NULL checks use the same forms as list_documents. `contains` is only valid for schema-confirmed text fields.",
|
|
658
|
+
"default": {},
|
|
659
|
+
},
|
|
660
|
+
"order_by": {"type": "string", "description": "Optional real column to sort by. Defaults to name."},
|
|
661
|
+
"order": {"type": "string", "enum": ["asc", "desc"], "description": "Sort direction (default asc).", "default": "asc"},
|
|
641
662
|
"limit": {"type": "integer", "description": "Optional max number of results. OMIT for no cap — returns ALL matches (e.g. to see the entire chart of accounts). Pass a number only to bound a large list."},
|
|
663
|
+
"offset": {"type": "integer", "description": "Skip this many matching rows. Use with limit for pagination.", "default": 0},
|
|
664
|
+
"result_fields": {
|
|
665
|
+
"type": "array",
|
|
666
|
+
"items": {"type": "string"},
|
|
667
|
+
"description": "Optional result projection. Returns only these real columns plus name. This is separate from `fields`, which selects columns searched by `query`.",
|
|
668
|
+
},
|
|
642
669
|
"include_disabled": {"type": "boolean", "description": "Default false (active records only). Set true to ALSO return disabled/archived records — needed to find a record you must inspect, re-enable, or reference, or to answer 'what's disabled'. Retry with this if a lookup for a record you know exists comes back empty."},
|
|
643
670
|
},
|
|
644
671
|
"required": ["master_type"],
|
|
@@ -649,7 +676,7 @@ TOOLS = [
|
|
|
649
676
|
"type": "function",
|
|
650
677
|
"function": {
|
|
651
678
|
"name": "get_master_fields",
|
|
652
|
-
"description": "List the available columns of a master type (customer, supplier, item, ...). Call this WHENEVER you're unsure which columns exist: before passing `fields` to search_masters, and before building a
|
|
679
|
+
"description": "List the available columns of a master type (customer, supplier, item, ...). Call this WHENEVER you're unsure which columns exist: before passing `fields` or `filters` to search_masters, and before building a create/update payload. Returns all fields, text fields (the columns that support `contains`), default free-text search fields, and bulk text fields that are searched only when explicitly named.",
|
|
653
680
|
"parameters": {
|
|
654
681
|
"type": "object",
|
|
655
682
|
"properties": {
|
|
@@ -787,10 +814,58 @@ TOOLS = [
|
|
|
787
814
|
"type": "function",
|
|
788
815
|
"function": {
|
|
789
816
|
"name": "list_chat_attachments",
|
|
790
|
-
"description": "List all files the user has uploaded in this chat session — PDFs, images, spreadsheets (Excel/CSV/ODS),
|
|
817
|
+
"description": "List all files the user has uploaded in this chat session — PDFs, images, spreadsheets (Excel/CSV/ODS), documents (Word/OpenDocument), and structured bank files (CAMT XML/ZIP). Returns metadata including id, filename, mime type, size, and upload date. Use this when the user references a previously uploaded file or you need its attachment id.",
|
|
791
818
|
"parameters": {"type": "object", "properties": {}},
|
|
792
819
|
},
|
|
793
820
|
},
|
|
821
|
+
{
|
|
822
|
+
"type": "function",
|
|
823
|
+
"function": {
|
|
824
|
+
"name": "preview_bank_statement_attachments",
|
|
825
|
+
"description": "Safely parse CAMT.053 XML or ZIP chat attachments and return a compact, read-only import preview: masked account, currency, period, balances, entry/detail/batch/QR counts, duplicates, warnings, and any exact Bank Account mapping. The raw XML is never added to the model context. Always call this before importing bank statements.",
|
|
826
|
+
"parameters": {
|
|
827
|
+
"type": "object",
|
|
828
|
+
"properties": {
|
|
829
|
+
"attachment_ids": {
|
|
830
|
+
"type": "array",
|
|
831
|
+
"items": {"type": "string"},
|
|
832
|
+
"minItems": 1,
|
|
833
|
+
"maxItems": 20,
|
|
834
|
+
"description": "CAMT XML/ZIP attachment ids from this chat.",
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
"required": ["attachment_ids"],
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
"type": "function",
|
|
843
|
+
"function": {
|
|
844
|
+
"name": "import_bank_statement_attachments",
|
|
845
|
+
"description": "Import previously previewed CAMT attachments as Bank Transactions without creating or posting any Payment Entry, Journal Entry, or GL entry. Exact file re-uploads and stable duplicate bank entries are skipped. Use only after showing the preview and receiving explicit user confirmation. Requires manager access and is unavailable in public demo mode.",
|
|
846
|
+
"parameters": {
|
|
847
|
+
"type": "object",
|
|
848
|
+
"properties": {
|
|
849
|
+
"attachment_ids": {
|
|
850
|
+
"type": "array",
|
|
851
|
+
"items": {"type": "string"},
|
|
852
|
+
"minItems": 1,
|
|
853
|
+
"maxItems": 20,
|
|
854
|
+
},
|
|
855
|
+
"mappings": {
|
|
856
|
+
"type": "object",
|
|
857
|
+
"additionalProperties": {"type": "string"},
|
|
858
|
+
"description": "Optional mapping of statement_key from the preview to Bank Account document name. Exact IBAN matches are used automatically.",
|
|
859
|
+
},
|
|
860
|
+
"confirmed": {
|
|
861
|
+
"type": "boolean",
|
|
862
|
+
"description": "Must be true, and only after the user explicitly confirmed the displayed preview.",
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
"required": ["attachment_ids", "confirmed"],
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
},
|
|
794
869
|
{
|
|
795
870
|
"type": "function",
|
|
796
871
|
"function": {
|
|
@@ -1275,15 +1350,20 @@ def build_tools(user_info: dict | None = None):
|
|
|
1275
1350
|
extra_docs = _extra_document_slugs()
|
|
1276
1351
|
extra_masters = _extra_master_types()
|
|
1277
1352
|
action_tools = services.registered_action_tools()
|
|
1353
|
+
hidden_tools: set[str] = set()
|
|
1278
1354
|
if user_info is not None:
|
|
1279
1355
|
role = user_info.get("role")
|
|
1280
1356
|
action_tools = [
|
|
1281
1357
|
tool for tool in action_tools
|
|
1282
1358
|
if services.registered_action_allowed(tool["function"]["name"], role)
|
|
1283
1359
|
]
|
|
1284
|
-
|
|
1360
|
+
if role not in {"manager", "admin"}:
|
|
1361
|
+
hidden_tools.add("import_bank_statement_attachments")
|
|
1362
|
+
if not extra_docs and not extra_masters and not action_tools and not hidden_tools:
|
|
1285
1363
|
return TOOLS
|
|
1286
1364
|
tools = copy.deepcopy(TOOLS)
|
|
1365
|
+
if hidden_tools:
|
|
1366
|
+
tools = [tool for tool in tools if tool["function"]["name"] not in hidden_tools]
|
|
1287
1367
|
for tool in tools:
|
|
1288
1368
|
props = tool["function"].get("parameters", {}).get("properties", {})
|
|
1289
1369
|
for schema in props.values():
|
|
@@ -1326,18 +1406,6 @@ def _handle_list_documents(args):
|
|
|
1326
1406
|
# within the tool-result budget. Use get_document to drill into one doc.
|
|
1327
1407
|
doctype = args["doctype"]
|
|
1328
1408
|
filters = args.get("filters") or {}
|
|
1329
|
-
# Chat callers routinely pass `search` but omit `search_fields`; with no fields
|
|
1330
|
-
# the free-text search silently no-ops and returns the newest N by creation
|
|
1331
|
-
# (2026-08-13: tag lookups on leads "found nothing"). Default the columns to the
|
|
1332
|
-
# doctype's registered chat fields so document search matches a record the way
|
|
1333
|
-
# search_masters already does for masters. The frontend never hits this — its
|
|
1334
|
-
# UI config always sends search_fields.
|
|
1335
|
-
if filters.get("search") and not filters.get("search_fields"):
|
|
1336
|
-
registered = (services.CHAT_DOCTYPES.get(doctype) or {}).get("fields") or []
|
|
1337
|
-
valid = set(services.document_columns(doctype) or [])
|
|
1338
|
-
default_fields = [f for f in registered if f in valid]
|
|
1339
|
-
if default_fields:
|
|
1340
|
-
filters = {**filters, "search_fields": default_fields}
|
|
1341
1409
|
err = _validate_filter_columns(doctype, filters)
|
|
1342
1410
|
if err:
|
|
1343
1411
|
return {"error": err}
|
|
@@ -1369,6 +1437,23 @@ def _handle_list_documents(args):
|
|
|
1369
1437
|
return rows
|
|
1370
1438
|
|
|
1371
1439
|
|
|
1440
|
+
def _handle_get_document_fields(args):
|
|
1441
|
+
doctype = args["doctype"]
|
|
1442
|
+
table = services.SLUG_TO_DOCTYPE.get(doctype)
|
|
1443
|
+
if not table:
|
|
1444
|
+
return {"error": f"Unknown document type: {doctype}"}
|
|
1445
|
+
db = get_db()
|
|
1446
|
+
cls = services.DOCUMENT_CLASSES.get(table)
|
|
1447
|
+
return {
|
|
1448
|
+
"doctype": doctype,
|
|
1449
|
+
"fields": sorted(db._get_table_columns(table)),
|
|
1450
|
+
"text_fields": sorted(db._get_text_columns(table)),
|
|
1451
|
+
"default_search_fields": services.document_search_columns(db, doctype),
|
|
1452
|
+
"child_tables": sorted((cls.CHILD_TABLES or {}).keys()) if cls else [],
|
|
1453
|
+
"link_fields": dict(getattr(cls, "LINK_FIELDS", None) or {}) if cls else {},
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
|
|
1372
1457
|
def _handle_get_document(args):
|
|
1373
1458
|
return services.load_document(args["doctype"], args["name"])
|
|
1374
1459
|
|
|
@@ -1407,22 +1492,6 @@ def _handle_convert_document(args):
|
|
|
1407
1492
|
return services.convert_document(args["doctype"], args["name"], args["target_doctype"])
|
|
1408
1493
|
|
|
1409
1494
|
|
|
1410
|
-
# Columns that are technically text but are noise for free-text master search.
|
|
1411
|
-
_MASTER_SEARCH_SKIP_COLUMNS = {
|
|
1412
|
-
"naming_series",
|
|
1413
|
-
"owner",
|
|
1414
|
-
"modified_by",
|
|
1415
|
-
"creation",
|
|
1416
|
-
"modified",
|
|
1417
|
-
"created_at",
|
|
1418
|
-
"updated_at",
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
# Large free-text columns excluded from the DEFAULT search: scanning/fuzzing a
|
|
1422
|
-
# big blob (e.g. an item description or an HTML template) is costly and rarely
|
|
1423
|
-
# how you identify a record. Still searchable on demand via the `fields` arg.
|
|
1424
|
-
_MASTER_BULK_TEXT_COLUMNS = {"description", "notes", "remarks", "terms", "comments"}
|
|
1425
|
-
|
|
1426
1495
|
# Minimum difflib similarity for a fuzzy (misspelled) match to be returned.
|
|
1427
1496
|
_MASTER_FUZZY_THRESHOLD = 0.6
|
|
1428
1497
|
# Bounds for the fuzzy scorer so a single large value can't blow up cost:
|
|
@@ -1431,18 +1500,9 @@ _FUZZY_MAX_VALUE_LEN = 200
|
|
|
1431
1500
|
_FUZZY_MAX_TOKENS = 16
|
|
1432
1501
|
|
|
1433
1502
|
|
|
1434
|
-
def _is_bulk_text_column(col):
|
|
1435
|
-
return (col in _MASTER_BULK_TEXT_COLUMNS
|
|
1436
|
-
or col.endswith("_template") or col.endswith("_html"))
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
1503
|
def _master_search_columns(db, doctype):
|
|
1440
|
-
"""
|
|
1441
|
-
|
|
1442
|
-
audit noise and large free-text/template columns."""
|
|
1443
|
-
cols = db._get_text_columns(doctype) - _MASTER_SEARCH_SKIP_COLUMNS
|
|
1444
|
-
# Deterministic order keeps generated SQL and fuzzy scoring stable.
|
|
1445
|
-
return sorted(c for c in cols if not _is_bulk_text_column(c))
|
|
1504
|
+
"""Compatibility wrapper for the shared REST/MCP field policy."""
|
|
1505
|
+
return services.master_search_columns(db, doctype)
|
|
1446
1506
|
|
|
1447
1507
|
|
|
1448
1508
|
def _fuzzy_master_search(db, doctype, search_cols, query, has_disabled, limit):
|
|
@@ -1485,10 +1545,11 @@ def _handle_get_master_fields(args):
|
|
|
1485
1545
|
return {"error": f"Unknown master type: {master_type}"}
|
|
1486
1546
|
doctype, _ = entry
|
|
1487
1547
|
default_search = _master_search_columns(db, doctype)
|
|
1488
|
-
bulk =
|
|
1548
|
+
bulk = services.master_bulk_text_columns(db, doctype)
|
|
1489
1549
|
return {
|
|
1490
1550
|
"master_type": master_type,
|
|
1491
1551
|
"fields": sorted(db._get_table_columns(doctype)),
|
|
1552
|
+
"text_fields": sorted(db._get_text_columns(doctype)),
|
|
1492
1553
|
# What search_masters searches when `fields` is omitted.
|
|
1493
1554
|
"default_search_fields": default_search,
|
|
1494
1555
|
# Large text fields searched ONLY when named in search_masters `fields`.
|
|
@@ -1517,23 +1578,20 @@ def _handle_search_masters(args):
|
|
|
1517
1578
|
return {"error": f"Unknown master type: {master_type}"}
|
|
1518
1579
|
|
|
1519
1580
|
doctype, _name_field = entry
|
|
1520
|
-
|
|
1581
|
+
columns = set(db._get_table_columns(doctype))
|
|
1582
|
+
has_disabled = "disabled" in columns
|
|
1521
1583
|
# Active-only by default (retired/archived records are hidden — you shouldn't
|
|
1522
1584
|
# book to them or wade through them). Set include_disabled to reach a disabled
|
|
1523
1585
|
# record you need to inspect, re-enable, or reference. Only meaningful when the
|
|
1524
1586
|
# table has a `disabled` column.
|
|
1525
1587
|
filter_disabled = has_disabled and not bool(args.get("include_disabled"))
|
|
1526
1588
|
|
|
1527
|
-
if not query:
|
|
1528
|
-
filters = {"disabled": 0} if filter_disabled else None
|
|
1529
|
-
return db.get_all(doctype, filters=filters, fields=["*"], limit=limit)
|
|
1530
|
-
|
|
1531
1589
|
# Optional `fields` narrows the search to specific columns — cheaper, more
|
|
1532
1590
|
# precise, and the only way to reach bulk columns (description, templates)
|
|
1533
1591
|
# that the default search skips. Unknown names are ignored.
|
|
1534
1592
|
requested = args.get("fields") or []
|
|
1535
1593
|
if requested:
|
|
1536
|
-
valid =
|
|
1594
|
+
valid = columns
|
|
1537
1595
|
# The identity alias (e.g. "item_code") is what the model calls the code
|
|
1538
1596
|
# everywhere else — create_master, document lines, the asset/reservation
|
|
1539
1597
|
# prompt — so it naturally passes fields=["item_code"]. But the real
|
|
@@ -1556,22 +1614,36 @@ def _handle_search_masters(args):
|
|
|
1556
1614
|
else:
|
|
1557
1615
|
search_cols = _master_search_columns(db, doctype)
|
|
1558
1616
|
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1617
|
+
filters = args.get("filters") or {}
|
|
1618
|
+
result_fields = args.get("result_fields") or None
|
|
1619
|
+
offset = args.get("offset", 0) or 0
|
|
1620
|
+
order_by = args.get("order_by")
|
|
1621
|
+
order = args.get("order", "asc")
|
|
1622
|
+
try:
|
|
1623
|
+
result = services.list_master_records(
|
|
1624
|
+
master_type,
|
|
1625
|
+
filters=filters,
|
|
1626
|
+
search=query or None,
|
|
1627
|
+
search_fields=search_cols if query else None,
|
|
1628
|
+
include_disabled=bool(args.get("include_disabled")),
|
|
1629
|
+
order_by=order_by,
|
|
1630
|
+
order=order,
|
|
1631
|
+
limit=limit,
|
|
1632
|
+
offset=offset,
|
|
1633
|
+
fields=result_fields,
|
|
1634
|
+
with_total=False,
|
|
1635
|
+
)
|
|
1636
|
+
except (TypeError, ValueError) as exc:
|
|
1637
|
+
return {"error": str(exc)}
|
|
1638
|
+
rows = [dict(row) for row in result["rows"]]
|
|
1639
|
+
if rows or not query:
|
|
1640
|
+
return rows
|
|
1641
|
+
|
|
1642
|
+
# Preserve the established fuzzy fallback for a simple lookup. A
|
|
1643
|
+
# deterministic field-filter query must return exactly its literal result;
|
|
1644
|
+
# relaxing only the free-text part could otherwise select the wrong record.
|
|
1645
|
+
if filters or offset or order_by or result_fields:
|
|
1646
|
+
return []
|
|
1575
1647
|
return _fuzzy_master_search(db, doctype, search_cols, query, filter_disabled, limit=limit)
|
|
1576
1648
|
|
|
1577
1649
|
|
|
@@ -1794,6 +1866,98 @@ def _handle_retrieve_chat_attachment(args, session_id: str | None = None, user_i
|
|
|
1794
1866
|
}
|
|
1795
1867
|
|
|
1796
1868
|
|
|
1869
|
+
def _load_camt_chat_attachments(args, user_id: str | None):
|
|
1870
|
+
attachment_ids = args.get("attachment_ids") or []
|
|
1871
|
+
if not user_id:
|
|
1872
|
+
return None, {"error": "No authenticated user for bank statement attachments."}
|
|
1873
|
+
if not isinstance(attachment_ids, list) or not 1 <= len(attachment_ids) <= 20:
|
|
1874
|
+
return None, {"error": "attachment_ids must contain between 1 and 20 attachment ids."}
|
|
1875
|
+
from api.attachments import get_attachments_by_ids
|
|
1876
|
+
from lambda_erp.accounting.camt import CamtError, parse_camt_upload
|
|
1877
|
+
|
|
1878
|
+
attachments = get_attachments_by_ids(attachment_ids, user_id)
|
|
1879
|
+
found = {item["id"] for item in attachments}
|
|
1880
|
+
missing = [item for item in attachment_ids if item not in found]
|
|
1881
|
+
if missing:
|
|
1882
|
+
return None, {"error": "One or more attachments were not found or access was denied."}
|
|
1883
|
+
documents = []
|
|
1884
|
+
try:
|
|
1885
|
+
for attachment in attachments:
|
|
1886
|
+
documents.extend(parse_camt_upload(attachment["data"], attachment["filename"]))
|
|
1887
|
+
except CamtError as exc:
|
|
1888
|
+
return None, {"error": str(exc)}
|
|
1889
|
+
return documents, None
|
|
1890
|
+
|
|
1891
|
+
|
|
1892
|
+
def _handle_preview_bank_statement_attachments(args, user_id: str | None = None):
|
|
1893
|
+
documents, error = _load_camt_chat_attachments(args, user_id)
|
|
1894
|
+
if error:
|
|
1895
|
+
return error
|
|
1896
|
+
from lambda_erp.accounting.bank_statement_import import preview_documents
|
|
1897
|
+
return preview_documents(documents)
|
|
1898
|
+
|
|
1899
|
+
|
|
1900
|
+
def _handle_import_bank_statement_attachments(args, user_info: dict | None = None):
|
|
1901
|
+
role = (user_info or {}).get("role")
|
|
1902
|
+
if role not in {"manager", "admin"}:
|
|
1903
|
+
return {"error": "Importing bank statements requires manager access and is unavailable in demo mode."}
|
|
1904
|
+
if args.get("confirmed") is not True:
|
|
1905
|
+
return {
|
|
1906
|
+
"error": (
|
|
1907
|
+
"Explicit confirmation is required. Show the bank statement preview, ask the user "
|
|
1908
|
+
"to confirm it, and call the import tool in a later turn with confirmed=true."
|
|
1909
|
+
)
|
|
1910
|
+
}
|
|
1911
|
+
user_id = (user_info or {}).get("name")
|
|
1912
|
+
documents, error = _load_camt_chat_attachments(args, user_id)
|
|
1913
|
+
if error:
|
|
1914
|
+
return error
|
|
1915
|
+
from lambda_erp.accounting.bank_statement_import import import_documents, preview_documents
|
|
1916
|
+
from lambda_erp.exceptions import ValidationError
|
|
1917
|
+
|
|
1918
|
+
mappings = args.get("mappings") or {}
|
|
1919
|
+
if not isinstance(mappings, dict):
|
|
1920
|
+
return {"error": "mappings must be an object of statement_key to Bank Account name."}
|
|
1921
|
+
# Exact IBAN matches are safe to fill automatically; anything ambiguous or
|
|
1922
|
+
# unmapped is returned to the model as a focused question for the user.
|
|
1923
|
+
preview = preview_documents(documents)
|
|
1924
|
+
effective_mappings = dict(mappings)
|
|
1925
|
+
missing = []
|
|
1926
|
+
for statement in preview["statements"]:
|
|
1927
|
+
if statement["already_imported"]:
|
|
1928
|
+
continue
|
|
1929
|
+
key = statement["statement_key"]
|
|
1930
|
+
matched = statement.get("matched_bank_account")
|
|
1931
|
+
if key not in effective_mappings and matched:
|
|
1932
|
+
effective_mappings[key] = matched["name"]
|
|
1933
|
+
if key not in effective_mappings:
|
|
1934
|
+
missing.append({
|
|
1935
|
+
"statement_key": key,
|
|
1936
|
+
"file_name": statement["file_name"],
|
|
1937
|
+
"account_iban_masked": statement["account_iban_masked"],
|
|
1938
|
+
"currency": statement["account_currency"],
|
|
1939
|
+
})
|
|
1940
|
+
if missing:
|
|
1941
|
+
return {
|
|
1942
|
+
"error": "Bank Account mapping is required before import.",
|
|
1943
|
+
"unmapped_statements": missing,
|
|
1944
|
+
}
|
|
1945
|
+
try:
|
|
1946
|
+
result = import_documents(
|
|
1947
|
+
documents, effective_mappings, imported_by=user_id
|
|
1948
|
+
)
|
|
1949
|
+
except ValidationError as exc:
|
|
1950
|
+
return {"error": str(exc)}
|
|
1951
|
+
# Keep the model context bounded even for multi-year statements.
|
|
1952
|
+
return {
|
|
1953
|
+
"imports": [
|
|
1954
|
+
{key: value for key, value in item.items() if key != "transactions"}
|
|
1955
|
+
for item in result["imports"]
|
|
1956
|
+
],
|
|
1957
|
+
"exact_duplicate_imports": result["exact_duplicate_imports"],
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
|
|
1797
1961
|
def _handle_query_dataset(args):
|
|
1798
1962
|
from api.routers.analytics import aggregate_semantic_dataset
|
|
1799
1963
|
|
|
@@ -1932,6 +2096,7 @@ def _handle_apply_company_setup(args):
|
|
|
1932
2096
|
|
|
1933
2097
|
TOOL_HANDLERS = {
|
|
1934
2098
|
"list_documents": _handle_list_documents,
|
|
2099
|
+
"get_document_fields": _handle_get_document_fields,
|
|
1935
2100
|
"get_document": _handle_get_document,
|
|
1936
2101
|
"create_document": _handle_create_document,
|
|
1937
2102
|
"update_document": _handle_update_document,
|
|
@@ -1950,6 +2115,8 @@ TOOL_HANDLERS = {
|
|
|
1950
2115
|
"retrieve_chat_history": lambda args: _handle_retrieve_chat_history(args),
|
|
1951
2116
|
"list_chat_attachments": _handle_list_chat_attachments,
|
|
1952
2117
|
"retrieve_chat_attachment": _handle_retrieve_chat_attachment,
|
|
2118
|
+
"preview_bank_statement_attachments": _handle_preview_bank_statement_attachments,
|
|
2119
|
+
"import_bank_statement_attachments": _handle_import_bank_statement_attachments,
|
|
1953
2120
|
"query_dataset": _handle_query_dataset,
|
|
1954
2121
|
"create_custom_analytics_report": lambda args: _handle_create_custom_analytics_report(args),
|
|
1955
2122
|
"get_custom_analytics_report": lambda args: _handle_get_custom_analytics_report(args),
|
|
@@ -2183,13 +2350,31 @@ When a user asks you to do something they don't have permission for, explain wha
|
|
|
2183
2350
|
## Available Document Types (use the slug when calling tools)
|
|
2184
2351
|
- **Selling:** quotation, proposal, sales-order, sales-invoice, pos-invoice
|
|
2185
2352
|
- **Buying:** purchase-order, purchase-invoice
|
|
2186
|
-
- **Accounting:** payment-entry, journal-entry, budget, subscription, bank-transaction
|
|
2353
|
+
- **Accounting:** payment-entry, journal-entry, budget, subscription, bank-account, bank-transaction
|
|
2187
2354
|
- **Stock:** stock-entry, delivery-note, purchase-receipt
|
|
2188
2355
|
- **Assets & hire:** asset, reservation
|
|
2189
2356
|
- **Settings:** pricing-rule{extension_doctypes_line}{chat_doctype_section}
|
|
2190
2357
|
|
|
2191
2358
|
## Document Workflow & What Each Document Does
|
|
2192
2359
|
|
|
2360
|
+
### CAMT bank statement imports
|
|
2361
|
+
CAMT XML/ZIP attachments are structured financial inputs. Never retrieve their raw contents into
|
|
2362
|
+
the conversation and never try to interpret the XML manually. Use this workflow:
|
|
2363
|
+
1. Find the attachment ids with `list_chat_attachments` when necessary.
|
|
2364
|
+
2. Call `preview_bank_statement_attachments` and present its period, masked account, currency,
|
|
2365
|
+
opening/closing balances, credits/debits, entry/detail/batch counts, QR-reference count,
|
|
2366
|
+
duplicate status, warnings, and proposed Bank Account mapping.
|
|
2367
|
+
3. If there is no exact mapping, list or create a `bank-account` document. It must link the external
|
|
2368
|
+
IBAN to the correct Company and GL Account; never guess that accounting mapping.
|
|
2369
|
+
4. Ask the user for explicit confirmation after showing the preview. Do not call the import tool in
|
|
2370
|
+
the same turn in which you first show the preview.
|
|
2371
|
+
5. Only after that confirmation, call `import_bank_statement_attachments` with `confirmed=true`.
|
|
2372
|
+
|
|
2373
|
+
The CAMT import creates unposted Bank Transactions and their batch details only. It never creates,
|
|
2374
|
+
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.
|
|
2377
|
+
|
|
2193
2378
|
### Sales Cycle
|
|
2194
2379
|
Quotation → Sales Order → Delivery Note (shipping) / Sales Invoice (billing) → Payment Entry
|
|
2195
2380
|
Shortcuts: Quotation can also convert directly to Sales Invoice or Delivery Note (skipping Sales Order) for quick deals.
|
|
@@ -2405,10 +2590,10 @@ Every master record has a primary key (the `name` column) and a human-readable d
|
|
|
2405
2590
|
When you fill in `item_code`, `customer`, `supplier`, `warehouse`, `company`, etc. in a document or child-table row, you MUST use the **primary key**, never the display name. `"item_code": "Project Management"` is ALWAYS wrong — it's a name, not a code.
|
|
2406
2591
|
|
|
2407
2592
|
If the user refers to something by its human name ("bill them 8 hours of project mgmt", "add Redstone to the quote"), resolve the key first:
|
|
2408
|
-
- `search_masters(master_type="item",
|
|
2593
|
+
- `search_masters(master_type="item", query="project management")` → returns `[{{name: "SVC-005", item_name: "Project Management"}}]`. Use `name` as `item_code`.
|
|
2409
2594
|
- Same for customers, suppliers, warehouses, etc. — `search_masters` is **case-insensitive** and falls back to **fuzzy matching for misspellings**, so a typo'd name ("Meynex") still resolves. Trust its results instead of concluding "not found" after one narrow try.
|
|
2410
2595
|
- **Prefer narrowing with `fields`** whenever you know the attribute: search a customer by city with `fields=["city"]`, by email with `fields=["contact_email"]`, etc. It's faster and avoids false matches from unrelated columns. Omitting `fields` searches all standard text fields (a good fallback when you're unsure where the value lives), but large free-text columns (e.g. item `description`) are only searched when you name them explicitly in `fields`.
|
|
2411
|
-
- **Don't guess column names** — call `get_master_fields(master_type=...)` first to see the real columns
|
|
2596
|
+
- **Don't guess column names** — call `get_master_fields(master_type=...)` first to see the real columns, which support `contains`, and which are searched by default. Use `query` + `fields` for a fuzzy lookup; use `filters` for exact, combined field conditions. For documents, use `get_document_fields` before constructing unfamiliar `list_documents` filters.
|
|
2412
2597
|
- **When unsure where a value belongs, discover the schema — don't guess or give up.** Before a `create_master`/`update_master` where you're not certain a field exists or which column fits (the user gives a contact person, a VAT/tax id, a payment term, an IBAN, …), call `get_master_fields(master_type=...)` and map the value onto the real column. Never tell the user you can't store something without checking the fields first — the master usually has a column for it (e.g. a customer's contact person goes in `contact_person`/`contact_phone`/`contact_email`, not the company-level `phone`/`email`).
|
|
2413
2598
|
|
|
2414
2599
|
When you list masters back to the user (items on an invoice, customers on a report), include the key in parentheses so follow-ups are unambiguous. Example: "Project Management (SVC-005) — 16 Hour".
|
|
@@ -3022,6 +3207,12 @@ async def run_thinking_loop(
|
|
|
3022
3207
|
tool_handlers["retrieve_chat_history"] = lambda args: _handle_retrieve_chat_history(args, session_id)
|
|
3023
3208
|
user_id_for_tools = user_info.get("name") if user_info else None
|
|
3024
3209
|
tool_handlers["list_chat_attachments"] = lambda args: _handle_list_chat_attachments(args, session_id, user_id_for_tools)
|
|
3210
|
+
tool_handlers["preview_bank_statement_attachments"] = (
|
|
3211
|
+
lambda args: _handle_preview_bank_statement_attachments(args, user_id_for_tools)
|
|
3212
|
+
)
|
|
3213
|
+
tool_handlers["import_bank_statement_attachments"] = (
|
|
3214
|
+
lambda args: _handle_import_bank_statement_attachments(args, user_info)
|
|
3215
|
+
)
|
|
3025
3216
|
_scoped_retrieve_attachment = (
|
|
3026
3217
|
lambda args: _handle_retrieve_chat_attachment(args, session_id, user_id_for_tools)
|
|
3027
3218
|
)
|
|
@@ -19,7 +19,7 @@ from api.auth import router as auth_router, COOKIE_NAME, decode_token
|
|
|
19
19
|
from api.oauth import router as oauth_router
|
|
20
20
|
from api.attachments import router as attachments_router
|
|
21
21
|
from api.chat import chat_websocket, router as chat_router
|
|
22
|
-
from api.routers import admin, documents, masters, reports, setup as setup_router, bank_reconciliation, analytics, accounting, proposals, chat_api, mcp, availability, actions
|
|
22
|
+
from api.routers import admin, documents, masters, reports, setup as setup_router, bank_reconciliation, bank_statements, analytics, accounting, proposals, chat_api, mcp, availability, actions
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def load_plugins() -> None:
|
|
@@ -112,6 +112,7 @@ app.include_router(reports.router, prefix="/api")
|
|
|
112
112
|
app.include_router(analytics.router, prefix="/api")
|
|
113
113
|
app.include_router(setup_router.router, prefix="/api")
|
|
114
114
|
app.include_router(bank_reconciliation.router, prefix="/api")
|
|
115
|
+
app.include_router(bank_statements.router, prefix="/api")
|
|
115
116
|
app.include_router(accounting.router, prefix="/api")
|
|
116
117
|
app.include_router(admin.router, prefix="/api")
|
|
117
118
|
app.include_router(chat_router, prefix="/api")
|