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.
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/PKG-INFO +1 -1
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/chat.py +70 -4
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/chat_api.py +14 -2
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/pyproject.toml +1 -1
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/.gitignore +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/LICENSE +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/README.md +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/__init__.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/attachments.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/auth.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/bootstrap.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/demo_limits.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/deps.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/errors.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/main.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/oauth.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/pdf.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/providers.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/remarks_md.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/__init__.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/accounting.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/admin.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/analytics.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/documents.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/masters.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/proposals.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/reports.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/routers/setup.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/services.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/templates/document.html +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/api/templates/proposal.html +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/docs/agents/README.md +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/frontend/README.md +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/frontend/src/api/client.ts +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/database.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/model.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.1.42 → lambda_erp-0.1.44}/terraform/README.md +0 -0
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|