lambda-erp 1.1.0__tar.gz → 1.1.1__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 (112) hide show
  1. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/PKG-INFO +1 -1
  2. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/chat.py +18 -12
  3. lambda_erp-1.1.1/api/chat_links.py +59 -0
  4. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/pyproject.toml +1 -1
  5. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/.gitignore +0 -0
  6. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/LICENSE +0 -0
  7. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/README.md +0 -0
  8. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/__init__.py +0 -0
  9. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/attachments.py +0 -0
  10. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/auth.py +0 -0
  11. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/bootstrap.py +0 -0
  12. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/demo_limits.py +0 -0
  13. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/deps.py +0 -0
  14. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/errors.py +0 -0
  15. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/list_values.py +0 -0
  16. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/main.py +0 -0
  17. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/oauth.py +0 -0
  18. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/pdf.py +0 -0
  19. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/pdf_contract.py +0 -0
  20. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/pdf_exports.py +0 -0
  21. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/pdf_profiles.py +0 -0
  22. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/providers.py +0 -0
  23. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/remarks_md.py +0 -0
  24. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/routers/__init__.py +0 -0
  25. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/routers/accounting.py +0 -0
  26. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/routers/actions.py +0 -0
  27. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/routers/admin.py +0 -0
  28. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/routers/analytics.py +0 -0
  29. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/routers/availability.py +0 -0
  30. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/routers/bank_reconciliation.py +0 -0
  31. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/routers/bank_statements.py +0 -0
  32. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/routers/chat_api.py +0 -0
  33. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/routers/documents.py +0 -0
  34. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/routers/masters.py +0 -0
  35. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/routers/mcp.py +0 -0
  36. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/routers/proposals.py +0 -0
  37. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/routers/reports.py +0 -0
  38. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/routers/setup.py +0 -0
  39. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/services.py +0 -0
  40. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/templates/_proposal_totals.html +0 -0
  41. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/templates/_record_base.html +0 -0
  42. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/templates/document.html +0 -0
  43. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/templates/proposal.html +0 -0
  44. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/templates/record.html +0 -0
  45. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/time_filters.py +0 -0
  46. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/api/tool_permissions.py +0 -0
  47. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/docs/agents/README.md +0 -0
  48. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/frontend/LICENSE +0 -0
  49. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/frontend/README.md +0 -0
  50. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/frontend/src/api/client.ts +0 -0
  51. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/__init__.py +0 -0
  52. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/__init__.py +0 -0
  53. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/bank_account.py +0 -0
  54. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/bank_reconciliation.py +0 -0
  55. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/bank_statement_import.py +0 -0
  56. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/bank_transaction.py +0 -0
  57. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/budget.py +0 -0
  58. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/camt.py +0 -0
  59. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  60. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/general_ledger.py +0 -0
  61. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/journal_entry.py +0 -0
  62. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/payment_entry.py +0 -0
  63. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/pos_invoice.py +0 -0
  64. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/purchase_invoice.py +0 -0
  65. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/revaluation.py +0 -0
  66. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/sales_invoice.py +0 -0
  67. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/settlement.py +0 -0
  68. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/__init__.py +0 -0
  69. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/engine.py +0 -0
  70. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/pack.py +0 -0
  71. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
  72. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/packs/ch.py +0 -0
  73. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
  74. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
  75. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
  76. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/packs/generic.py +0 -0
  77. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/profiles.py +0 -0
  78. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/spine.py +0 -0
  79. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/accounting/subscription.py +0 -0
  80. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/assets/__init__.py +0 -0
  81. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/assets/asset.py +0 -0
  82. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/assets/lifecycle.py +0 -0
  83. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/assets/reservation.py +0 -0
  84. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/buying/__init__.py +0 -0
  85. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/buying/purchase_order.py +0 -0
  86. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/controllers/__init__.py +0 -0
  87. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/controllers/currency.py +0 -0
  88. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/controllers/defaults.py +0 -0
  89. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/controllers/item_price.py +0 -0
  90. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/controllers/item_prices.py +0 -0
  91. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/controllers/pricing_rule.py +0 -0
  92. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  93. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/database.py +0 -0
  94. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/exceptions.py +0 -0
  95. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/hooks.py +0 -0
  96. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/master_integrity.py +0 -0
  97. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/model.py +0 -0
  98. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/selling/__init__.py +0 -0
  99. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/selling/proposal.py +0 -0
  100. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/selling/quotation.py +0 -0
  101. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/selling/sales_order.py +0 -0
  102. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/simulation.py +0 -0
  103. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/stock/__init__.py +0 -0
  104. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/stock/delivery_note.py +0 -0
  105. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/stock/purchase_receipt.py +0 -0
  106. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/stock/stock_entry.py +0 -0
  107. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/stock/stock_ledger.py +0 -0
  108. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/timestamps.py +0 -0
  109. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/utils.py +0 -0
  110. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/validation.py +0 -0
  111. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/lambda_erp/workflow.py +0 -0
  112. {lambda_erp-1.1.0 → lambda_erp-1.1.1}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lambda-erp
3
- Version: 1.1.0
3
+ Version: 1.1.1
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: Apache-2.0
@@ -28,6 +28,7 @@ from openai import OpenAI
28
28
 
29
29
  from api.time_filters import TIME_FILTER_SCHEMA, time_filter_fields
30
30
  from api import services
31
+ from api.chat_links import with_record_view_urls
31
32
  from api.tool_permissions import tool_allowed, tool_permission_error
32
33
  from api.demo_limits import (
33
34
  demo_call_reserve_usd,
@@ -1459,6 +1460,7 @@ def _validate_filter_columns(doctype_slug, filters) -> str | None:
1459
1460
  return None
1460
1461
 
1461
1462
 
1463
+ @with_record_view_urls("doctype")
1462
1464
  def _handle_list_documents(args):
1463
1465
  doctype = args["doctype"]
1464
1466
  try:
@@ -1497,10 +1499,12 @@ def _handle_get_document_fields(args):
1497
1499
  return services.document_field_metadata(args["doctype"])
1498
1500
 
1499
1501
 
1502
+ @with_record_view_urls("doctype")
1500
1503
  def _handle_get_document(args):
1501
1504
  return services.load_document(args["doctype"], args["name"])
1502
1505
 
1503
1506
 
1507
+ @with_record_view_urls("doctype")
1504
1508
  def _handle_create_document(args):
1505
1509
  data = args.get("data")
1506
1510
  if not data:
@@ -1508,10 +1512,12 @@ def _handle_create_document(args):
1508
1512
  return services.create_document(args["doctype"], data)
1509
1513
 
1510
1514
 
1515
+ @with_record_view_urls("doctype")
1511
1516
  def _handle_update_document(args):
1512
1517
  return services.update_document(args["doctype"], args["name"], args.get("data", {}))
1513
1518
 
1514
1519
 
1520
+ @with_record_view_urls("doctype")
1515
1521
  def _handle_batch_update_documents(args):
1516
1522
  try:
1517
1523
  return services.batch_update_documents(args["doctype"], args.get("updates"))
@@ -1519,18 +1525,22 @@ def _handle_batch_update_documents(args):
1519
1525
  return {"error": str(e)}
1520
1526
 
1521
1527
 
1528
+ @with_record_view_urls("doctype")
1522
1529
  def _handle_submit_document(args):
1523
1530
  return services.submit_document(args["doctype"], args["name"])
1524
1531
 
1525
1532
 
1533
+ @with_record_view_urls("doctype")
1526
1534
  def _handle_cancel_document(args):
1527
1535
  return services.cancel_document(args["doctype"], args["name"])
1528
1536
 
1529
1537
 
1538
+ @with_record_view_urls("doctype")
1530
1539
  def _handle_discard_document(args):
1531
1540
  return services.discard_document(args["doctype"], args["name"])
1532
1541
 
1533
1542
 
1543
+ @with_record_view_urls("target_doctype")
1534
1544
  def _handle_convert_document(args):
1535
1545
  return services.convert_document(args["doctype"], args["name"], args["target_doctype"])
1536
1546
 
@@ -1607,6 +1617,7 @@ def _handle_get_master_fields(args):
1607
1617
  }
1608
1618
 
1609
1619
 
1620
+ @with_record_view_urls("master_type", master=True)
1610
1621
  def _handle_search_masters(args):
1611
1622
  db = get_db()
1612
1623
  meta = bool(args.get("include_meta")) or args.get("time_filter") is not None
@@ -1724,6 +1735,7 @@ def _ignored_master_fields(master_type: str, data: dict) -> list[str]:
1724
1735
  return [k for k in data.keys() if k not in valid]
1725
1736
 
1726
1737
 
1738
+ @with_record_view_urls("master_type", master=True)
1727
1739
  def _handle_create_master(args):
1728
1740
  master_type = args["master_type"]
1729
1741
  data = args.get("data")
@@ -1750,6 +1762,7 @@ def _handle_create_master(args):
1750
1762
  return result
1751
1763
 
1752
1764
 
1765
+ @with_record_view_urls("master_type", master=True)
1753
1766
  def _handle_update_master(args):
1754
1767
  master_type = args["master_type"]
1755
1768
  name = args.get("name")
@@ -2464,18 +2477,11 @@ Examples:
2464
2477
  - Correct: `[SINV-0012](/app/sales-invoice/SINV-0012)`
2465
2478
  - Avoid: `/reports/analytics?report_id=RPT-AB12CD34` (bare URL)"""
2466
2479
  document_links_section = """## Document & Master Links
2467
- When referencing records, always use clickable markdown links so the user can open them directly.
2468
-
2469
- **Documents** (quotations, invoices, orders, deliveries, receipts, payments, journal entries, stock entries):
2470
- - **View/edit link:** `/app/{doctype-slug}/{name}` — e.g. [SINV-0001](/app/sales-invoice/SINV-0001)
2471
- - **PDF files:** call generate_document_pdf. Only link the returned download_url after successful generation; never construct PDF links yourself.
2472
- The doctype slug is the lowercase, hyphenated form: sales-invoice, purchase-order, delivery-note, etc.
2473
-
2474
- **Master records** (customer, supplier, item, warehouse, company):
2475
- - **View/edit link:** `/masters/{master-type}/{name}` — e.g. [SUPP-001](/masters/supplier/SUPP-001), [CUST-003](/masters/customer/CUST-003), [ITEM-001](/masters/item/ITEM-001)
2476
- - NEVER use `/app/...` for masters — that path is only for transactional documents.
2477
-
2478
- Always include the view link after creating, submitting, converting, or updating a record. Call generate_document_pdf when a printable file is requested. Only confirm and link a successfully generated file."""
2480
+ Document and master tools return a canonical `view_url` on each record, including list/search rows and successful batch results. When linking a record, copy that exact value into a markdown link `[label](view_url)`. Never construct a record URL yourself, add a `#`, change the path, or re-encode the returned URL.
2481
+ `view_url` is tool output metadata, not a document/master field; never include it in create/update data.
2482
+ If `view_url` is null, refer to the record in plain text. For a projected document list or batch result missing parent-page information, call get_document to obtain its view_url if a link is needed. Some records have no page at all.
2483
+ Always include the returned view link, when available, after creating, submitting, converting, or updating a record.
2484
+ For PDF files, call generate_document_pdf and copy its returned download_url only after successful generation. Never construct PDF links yourself."""
2479
2485
 
2480
2486
  if user_role == "admin":
2481
2487
  role_desc = "You have **admin** access — full permissions to create, edit, submit, cancel documents, manage master data, run reports, and manage users."
@@ -0,0 +1,59 @@
1
+ """Canonical record links on chat/MCP tool results, never persisted fields."""
2
+
3
+ from functools import wraps
4
+ from urllib.parse import quote, urlencode
5
+
6
+ from api import services
7
+
8
+
9
+ def record_view_url(record_type: str, record: dict, *, master: bool = False) -> str | None:
10
+ name = record.get("name")
11
+ if not name:
12
+ return None
13
+ if master:
14
+ if record_type not in services.MASTER_TABLES or record_type == "cost-center":
15
+ return None
16
+ if record_type == "account":
17
+ return "/reports/general-ledger?" + urlencode({"account": name})
18
+ return f"/masters/{quote(record_type, safe='')}/{quote(str(name), safe='')}"
19
+
20
+ slug = services.DOCTYPE_TO_SLUG.get(record_type, record_type)
21
+ if slug not in services.SLUG_TO_DOCTYPE:
22
+ return None
23
+ page = services.chat_doctype_page_info(slug)
24
+ if page:
25
+ if page["kind"] == "none":
26
+ return None
27
+ if page["kind"] == "via":
28
+ name = record.get(page["link_field"])
29
+ if not name:
30
+ # Projected list/batch results may omit the parent. The model
31
+ # can get_document to obtain it; do not guess a destination.
32
+ return None
33
+ slug = page["parent_slug"]
34
+ return f"/app/{quote(slug, safe='')}/{quote(str(name), safe='')}"
35
+
36
+
37
+ def with_record_view_urls(type_arg: str, *, master: bool = False):
38
+ """Enrich records and list/batch envelopes without changing service data."""
39
+ def decorate(handler):
40
+ @wraps(handler)
41
+ def wrapped(args):
42
+ result = handler(args)
43
+
44
+ def enrich(row):
45
+ if not isinstance(row, dict) or "error" in row or row.get("ok") is False:
46
+ return row
47
+ return {**row, "view_url": record_view_url(args.get(type_arg), row, master=master)}
48
+
49
+ if isinstance(result, list):
50
+ return [enrich(row) for row in result]
51
+ if isinstance(result, dict) and "error" not in result:
52
+ if "name" not in result:
53
+ for key in ("rows", "results"):
54
+ if isinstance(result.get(key), list):
55
+ return {**result, key: [enrich(row) for row in result[key]]}
56
+ return enrich(result)
57
+ return result
58
+ return wrapped
59
+ return decorate
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "1.1.0"
3
+ version = "1.1.1"
4
4
  description = "Core ERP logic - accounting, sales, purchasing, inventory"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes