lambda-erp 0.1.42__tar.gz → 0.1.44__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 (71) hide show
  1. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/PKG-INFO +1 -1
  2. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/chat.py +70 -4
  3. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/chat_api.py +14 -2
  4. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/pyproject.toml +1 -1
  5. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/.gitignore +0 -0
  6. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/LICENSE +0 -0
  7. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/README.md +0 -0
  8. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/__init__.py +0 -0
  9. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/attachments.py +0 -0
  10. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/auth.py +0 -0
  11. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/bootstrap.py +0 -0
  12. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/demo_limits.py +0 -0
  13. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/deps.py +0 -0
  14. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/errors.py +0 -0
  15. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/main.py +0 -0
  16. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/oauth.py +0 -0
  17. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/pdf.py +0 -0
  18. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/providers.py +0 -0
  19. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/remarks_md.py +0 -0
  20. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/__init__.py +0 -0
  21. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/accounting.py +0 -0
  22. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/admin.py +0 -0
  23. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/analytics.py +0 -0
  24. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/bank_reconciliation.py +0 -0
  25. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/documents.py +0 -0
  26. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/masters.py +0 -0
  27. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/proposals.py +0 -0
  28. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/reports.py +0 -0
  29. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/setup.py +0 -0
  30. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/services.py +0 -0
  31. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/templates/document.html +0 -0
  32. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/templates/proposal.html +0 -0
  33. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/docs/agents/README.md +0 -0
  34. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/frontend/README.md +0 -0
  35. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/frontend/src/api/client.ts +0 -0
  36. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/__init__.py +0 -0
  37. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/__init__.py +0 -0
  38. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/bank_transaction.py +0 -0
  39. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/budget.py +0 -0
  40. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  41. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/general_ledger.py +0 -0
  42. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/journal_entry.py +0 -0
  43. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/payment_entry.py +0 -0
  44. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/pos_invoice.py +0 -0
  45. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/purchase_invoice.py +0 -0
  46. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/revaluation.py +0 -0
  47. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/sales_invoice.py +0 -0
  48. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/subscription.py +0 -0
  49. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/buying/__init__.py +0 -0
  50. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/buying/purchase_order.py +0 -0
  51. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/controllers/__init__.py +0 -0
  52. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/controllers/currency.py +0 -0
  53. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/controllers/defaults.py +0 -0
  54. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/controllers/pricing_rule.py +0 -0
  55. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  56. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/database.py +0 -0
  57. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/exceptions.py +0 -0
  58. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/hooks.py +0 -0
  59. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/model.py +0 -0
  60. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/selling/__init__.py +0 -0
  61. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/selling/proposal.py +0 -0
  62. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/selling/quotation.py +0 -0
  63. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/selling/sales_order.py +0 -0
  64. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/simulation.py +0 -0
  65. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/stock/__init__.py +0 -0
  66. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/stock/delivery_note.py +0 -0
  67. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/stock/purchase_receipt.py +0 -0
  68. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/stock/stock_entry.py +0 -0
  69. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/stock/stock_ledger.py +0 -0
  70. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/utils.py +0 -0
  71. {lambda_erp-0.1.42 → lambda_erp-0.1.44}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-erp
3
- Version: 0.1.42
3
+ Version: 0.1.44
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: MIT
@@ -622,7 +622,7 @@ TOOLS = [
622
622
  "Every record's id is `name`. Customer/Supplier/Item/Warehouse auto-generate it (CUST-/SUPP-/ITEM-/WH-NNN) when omitted; Company/Account/Cost Center have NO auto-id, so `name` is required for them.\n\n"
623
623
  "**Customer fields:** name (optional custom id; auto CUST-NNN), customer_name (required), customer_group, territory, default_currency, credit_limit, email, phone, address, city, zip_code, country, tax_id, contact_person (named contact at the customer), contact_email, contact_phone.\n"
624
624
  "**Supplier fields:** name (optional custom id; auto SUPP-NNN), supplier_name (required), supplier_group, default_currency, email, phone, address, city, zip_code, country, tax_id.\n"
625
- "**Item fields:** item_code (the unique item code/id, e.g. \"SVC-SPARK\" — optional, may use ANY prefix, not just ITEM; auto-generated as ITEM-NNN if omitted), item_name (required, the human-readable name), item_group, stock_uom, standard_rate, is_stock_item, default_warehouse, description.\n"
625
+ "**Item fields:** item_code (the unique item code/id, e.g. \"SVC-SPARK\" — optional, may use ANY prefix, not just ITEM; auto-generated as ITEM-NNN if omitted), item_name (required, the human-readable name), item_group, stock_uom (prefer one of the units listed under 'Item Units' in the system context — exact spelling; a new unit is allowed when none fits), standard_rate, is_stock_item, default_warehouse, description.\n"
626
626
  "**Warehouse fields:** name (optional custom id; auto WH-NNN), warehouse_name (required), company, parent_warehouse (omit or null when not needed), address, city, zip_code, country.\n"
627
627
  "**Company fields:** name (the company id — optional, defaults to company_name), company_name (required), default_currency, email, phone, address, city, zip_code, country, tax_id.\n"
628
628
  "**Account fields:** name (REQUIRED — full account id, conventionally \"<account_name> - <company abbr>\", e.g. \"Marketing Expenses - LAMB\"), account_name (required), company (required), root_type (Asset/Liability/Equity/Income/Expense), report_type (\"Balance Sheet\" or \"Profit and Loss\"), account_type (e.g. Receivable, Payable, Bank, Cash, Stock, Tax), parent_account, account_currency, is_group (0/1).\n"
@@ -1623,11 +1623,37 @@ def _prompt_analytics_context() -> str:
1623
1623
  )
1624
1624
 
1625
1625
 
1626
+ def _prompt_uom_context() -> str:
1627
+ """Dynamic list of the item units (stock_uom) currently in use.
1628
+
1629
+ The unit list is NOT a fixed vocabulary — users add new units simply by
1630
+ using them — so the guidance is rebuilt from the Item table on every turn
1631
+ rather than hardcoded. Nudging the agent toward existing spellings keeps
1632
+ items consistent (and matching the GUI's dropdown), while still allowing a
1633
+ genuinely new unit when none fits.
1634
+ """
1635
+ try:
1636
+ rows = get_db().get_all("Item", fields=["stock_uom"], limit=1000)
1637
+ except Exception:
1638
+ return ""
1639
+ uoms = sorted({(row.get("stock_uom") or "").strip() for row in rows} - {""})
1640
+ if not uoms:
1641
+ return ""
1642
+ return (
1643
+ "## Item Units (stock_uom)\n"
1644
+ f"Units currently in use in this ERP: {', '.join(uoms)}.\n"
1645
+ "When creating or editing items, PREFER one of these exact spellings so "
1646
+ "items stay consistent. A new unit is allowed when none of them fits — "
1647
+ "use a short singular noun (e.g. \"Stück\", \"Bottle\", \"Hour\").\n"
1648
+ )
1649
+
1650
+
1626
1651
  def build_system_prompt(user_info: dict | None = None, channel: str = "web"):
1627
1652
  user_name = user_info.get("full_name", "User") if user_info else "User"
1628
1653
  user_role = user_info.get("role", "viewer") if user_info else "viewer"
1629
1654
  company_context = _prompt_company_context()
1630
1655
  analytics_context = _prompt_analytics_context()
1656
+ uom_context = _prompt_uom_context()
1631
1657
 
1632
1658
  # Channel-aware link guidance. On the "web" channel the reader is a browser
1633
1659
  # inside the ERP, so web-relative links are clickable. On the "api" channel the
@@ -1697,7 +1723,7 @@ You are speaking with **{user_name}** (role: **{user_role}**).
1697
1723
 
1698
1724
  {company_context}
1699
1725
  {analytics_context}
1700
-
1726
+ {uom_context}
1701
1727
  ## User Roles
1702
1728
  Lambda ERP has four roles:
1703
1729
  - **admin**: Full access to everything — documents, masters, reports, company setup, and user management (inviting team members, changing roles).
@@ -1788,6 +1814,13 @@ When you parse a supplier invoice PDF that has both products and a shipping line
1788
1814
 
1789
1815
  If multiple charges stack (freight + VAT on total incl. freight), put freight first as `Actual`, then VAT second as `On Previous Row Total` referencing the freight row's `idx`.
1790
1816
 
1817
+ ### Adding VAT to a SALES document (quotation / sales order / sales invoice)
1818
+
1819
+ "Add 8.1% VAT / MwSt" needs exactly ONE `taxes[]` row — do it directly with update_document:
1820
+ - `charge_type`: `"On Net Total"`, `rate`: the percentage (e.g. 8.1), `description`: the label (e.g. "MwSt. 8.1%").
1821
+ - `account_head` is OPTIONAL on draft sales documents. If a Tax Payable-like account is already known, use it — but do NOT go hunting for one: if it isn't found in a single lookup, ADD THE ROW WITHOUT `account_head`. Totals still compute (net → tax amount → grand total). NEVER refuse or stall the request because no tax account exists; add the rate-only row and mention the missing account in one sentence.
1822
+ - Then verify with one get_document call and report net / tax amount / gross.
1823
+
1791
1824
  ### Returns (Credit Notes / Debit Notes)
1792
1825
  Returns use the SAME document type with negative quantities and `is_return=1`. Create them by "converting" a document to a return of the same type.
1793
1826
 
@@ -2260,12 +2293,16 @@ async def generate_title(
2260
2293
  # Reasoning loop
2261
2294
  # ---------------------------------------------------------------------------
2262
2295
 
2296
+ # The agentic orchestrator model. Function tools on /v1/chat/completions
2297
+ # require reasoning_effort="none" for the gpt-5.6 family (400 otherwise).
2298
+ ORCHESTRATOR_MODEL = "gpt-5.6-terra"
2299
+
2263
2300
 
2264
2301
  async def run_thinking_loop(
2265
2302
  messages: list[dict],
2266
2303
  on_event,
2267
2304
  session_id: str = None,
2268
- max_iterations: int = 8,
2305
+ max_iterations: int = 12,
2269
2306
  user_info: dict | None = None,
2270
2307
  client_ip: str | None = None,
2271
2308
  ):
@@ -2368,7 +2405,7 @@ async def run_thinking_loop(
2368
2405
  # on ANY exit path — including asyncio.CancelledError raised after
2369
2406
  # the SDK call has already returned but before settle() ran.
2370
2407
  settled = False
2371
- model_name = "gpt-5.6-terra"
2408
+ model_name = ORCHESTRATOR_MODEL
2372
2409
  try:
2373
2410
  print(f"[chat_llm] provider=openai model={model_name} session_id={session_id or '-'} iter={iteration + 1}", flush=True)
2374
2411
  await on_event({"type": "llm_provider", "provider": "openai", "model": model_name})
@@ -2514,7 +2551,36 @@ async def run_thinking_loop(
2514
2551
  ],
2515
2552
  })
2516
2553
 
2554
+ # Step cap reached. The canned "check the results above" is a dead end on the
2555
+ # API channel — the REST surface returns only this final text, so there is no
2556
+ # "above" for a headless orchestrator. Ask the model for ONE tool-free wrap-up
2557
+ # that states concretely what was and wasn't changed, so the caller can react.
2558
+ # Demo visitors keep the canned text (no spend outside the per-iteration
2559
+ # reservations); any failure falls back to the canned text.
2517
2560
  content = "I've reached the maximum number of reasoning steps. Here's what I've done so far — please check the results above."
2561
+ if not demo_mode:
2562
+ try:
2563
+ wrap = await asyncio.to_thread(
2564
+ openai_client.chat.completions.create,
2565
+ model=ORCHESTRATOR_MODEL,
2566
+ messages=messages + [{
2567
+ "role": "user",
2568
+ "content": (
2569
+ "You hit the step limit before finishing. In the user's language, state "
2570
+ "concretely: (1) what you changed (which documents/records, with amounts), "
2571
+ "(2) what you did NOT get done and why, (3) the current state of the affected "
2572
+ "record(s) as far as you know it. Be specific — names and numbers. This text "
2573
+ "stands alone: do NOT refer to 'the results above' or to earlier messages."
2574
+ ),
2575
+ }],
2576
+ max_completion_tokens=600,
2577
+ reasoning_effort="none",
2578
+ )
2579
+ wrap_text = (wrap.choices[0].message.content or "").strip()
2580
+ if wrap_text:
2581
+ content = wrap_text
2582
+ except Exception:
2583
+ pass
2518
2584
  messages.append({"role": "assistant", "content": content})
2519
2585
  await on_event({"type": "complete", "content": content})
2520
2586
 
@@ -58,11 +58,23 @@ def _extract_documents(reply: str, request: Request) -> list[dict]:
58
58
  if (doctype_slug, name) in seen:
59
59
  continue
60
60
  seen.add((doctype_slug, name))
61
- documents.append({
61
+ entry = {
62
62
  "doctype": doctype_slug,
63
63
  "name": name,
64
64
  "pdf_url": f"{base}/api/v1/documents/{doctype_slug}/{name}/pdf",
65
- })
65
+ }
66
+ # Version stamp: a document can change after an earlier turn fetched its
67
+ # PDF, making that attachment stale. `modified` lets the caller tell v1
68
+ # from v2 (e.g. timestamped attachment filenames) and reason about which
69
+ # is current. Best-effort — a vanished doc still gets its pdf_url.
70
+ try:
71
+ doc = load_document(doctype_slug, name)
72
+ modified = doc.get("modified") if isinstance(doc, dict) else None
73
+ if modified:
74
+ entry["modified"] = str(modified)
75
+ except Exception:
76
+ pass
77
+ documents.append(entry)
66
78
  return documents
67
79
 
68
80
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.1.42"
3
+ version = "0.1.44"
4
4
  description = "Core ERP logic - accounting, sales, purchasing, inventory"
5
5
  readme = "README.md"
6
6
  license = "MIT"
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