lambda-erp 0.1.43__tar.gz → 0.1.45__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.43 → lambda_erp-0.1.45}/PKG-INFO +1 -1
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/chat.py +28 -2
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/routers/reports.py +106 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/frontend/src/api/client.ts +5 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/pyproject.toml +1 -1
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/.gitignore +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/LICENSE +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/README.md +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/__init__.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/attachments.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/auth.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/bootstrap.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/demo_limits.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/deps.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/errors.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/main.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/oauth.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/pdf.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/providers.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/remarks_md.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/routers/__init__.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/routers/accounting.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/routers/admin.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/routers/analytics.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/routers/documents.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/routers/masters.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/routers/proposals.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/routers/setup.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/services.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/templates/document.html +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/api/templates/proposal.html +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/docs/agents/README.md +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/frontend/README.md +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/database.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/model.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.1.43 → lambda_erp-0.1.45}/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).
|
|
@@ -121,6 +121,112 @@ def trial_balance(
|
|
|
121
121
|
company, presentation_currency, to_date)
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
def _chart_of_accounts(db, company=None, from_date=None, to_date=None):
|
|
125
|
+
"""Chart of accounts (every account, incl. groups and zero-balance ones)
|
|
126
|
+
with period balances — the tree the GUI renders.
|
|
127
|
+
|
|
128
|
+
Balance semantics follow standard presentation:
|
|
129
|
+
- Balance Sheet accounts: CLOSING balance as of ``to_date`` (all postings
|
|
130
|
+
up to and including it; ``from_date`` is ignored for them).
|
|
131
|
+
- P&L accounts: movement WITHIN [from_date, to_date].
|
|
132
|
+
Values are natural-signed per root_type (Asset/Expense: debit − credit;
|
|
133
|
+
Liability/Equity/Income: credit − debit) so revenue reads positive.
|
|
134
|
+
Group accounts roll up every descendant's balance (plus any postings of
|
|
135
|
+
their own, though entries normally land on leaves).
|
|
136
|
+
"""
|
|
137
|
+
acc_filters = {}
|
|
138
|
+
if company:
|
|
139
|
+
acc_filters["company"] = company
|
|
140
|
+
accounts = db.get_all(
|
|
141
|
+
"Account", filters=acc_filters,
|
|
142
|
+
fields=["name", "account_name", "parent_account", "root_type",
|
|
143
|
+
"report_type", "account_type", "is_group", "disabled"],
|
|
144
|
+
order_by="name",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def _movement(where_extra, params_extra):
|
|
148
|
+
clause = "is_cancelled = 0" + where_extra
|
|
149
|
+
params = list(params_extra)
|
|
150
|
+
if company:
|
|
151
|
+
clause += " AND company = ?"
|
|
152
|
+
params.append(company)
|
|
153
|
+
found = db.sql(
|
|
154
|
+
f"""SELECT account,
|
|
155
|
+
COALESCE(SUM(debit), 0) AS d,
|
|
156
|
+
COALESCE(SUM(credit), 0) AS c
|
|
157
|
+
FROM "GL Entry" WHERE {clause} GROUP BY account""",
|
|
158
|
+
params,
|
|
159
|
+
)
|
|
160
|
+
return {r["account"]: (flt(r["d"], 2), flt(r["c"], 2)) for r in found}
|
|
161
|
+
|
|
162
|
+
bs_where, bs_params = "", []
|
|
163
|
+
if to_date:
|
|
164
|
+
bs_where, bs_params = " AND posting_date <= ?", [to_date]
|
|
165
|
+
bs_map = _movement(bs_where, bs_params)
|
|
166
|
+
|
|
167
|
+
if from_date or to_date:
|
|
168
|
+
pl_where, pl_params = "", []
|
|
169
|
+
if from_date:
|
|
170
|
+
pl_where += " AND posting_date >= ?"
|
|
171
|
+
pl_params.append(from_date)
|
|
172
|
+
if to_date:
|
|
173
|
+
pl_where += " AND posting_date <= ?"
|
|
174
|
+
pl_params.append(to_date)
|
|
175
|
+
pl_map = _movement(pl_where, pl_params)
|
|
176
|
+
else:
|
|
177
|
+
pl_map = bs_map
|
|
178
|
+
|
|
179
|
+
rows = []
|
|
180
|
+
by_name = {}
|
|
181
|
+
for acc in accounts:
|
|
182
|
+
is_pl = acc.get("report_type") == "Profit and Loss"
|
|
183
|
+
d, c = (pl_map if is_pl else bs_map).get(acc["name"], (0.0, 0.0))
|
|
184
|
+
if acc.get("root_type") in ("Liability", "Equity", "Income"):
|
|
185
|
+
natural = c - d
|
|
186
|
+
else:
|
|
187
|
+
natural = d - c
|
|
188
|
+
row = {
|
|
189
|
+
"name": acc["name"],
|
|
190
|
+
"account_name": acc.get("account_name"),
|
|
191
|
+
"parent_account": acc.get("parent_account") or None,
|
|
192
|
+
"root_type": acc.get("root_type"),
|
|
193
|
+
"account_type": acc.get("account_type"),
|
|
194
|
+
"is_group": bool(acc.get("is_group")),
|
|
195
|
+
"disabled": bool(acc.get("disabled")),
|
|
196
|
+
"balance": flt(natural, 2),
|
|
197
|
+
"has_entries": bool(d or c),
|
|
198
|
+
}
|
|
199
|
+
rows.append(row)
|
|
200
|
+
by_name[row["name"]] = row
|
|
201
|
+
|
|
202
|
+
# Roll every account's own balance up its ancestor chain so group rows
|
|
203
|
+
# show subtree totals.
|
|
204
|
+
subtree_extra = {r["name"]: 0.0 for r in rows}
|
|
205
|
+
for row in rows:
|
|
206
|
+
parent = row["parent_account"]
|
|
207
|
+
while parent and parent in by_name:
|
|
208
|
+
subtree_extra[parent] += row["balance"]
|
|
209
|
+
parent = by_name[parent]["parent_account"]
|
|
210
|
+
for row in rows:
|
|
211
|
+
if row["is_group"]:
|
|
212
|
+
row["balance"] = flt(row["balance"] + subtree_extra[row["name"]], 2)
|
|
213
|
+
row["has_entries"] = row["has_entries"] or bool(subtree_extra[row["name"]])
|
|
214
|
+
|
|
215
|
+
return {"accounts": rows, "from_date": from_date, "to_date": to_date}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@router.get("/chart-of-accounts")
|
|
219
|
+
def chart_of_accounts(
|
|
220
|
+
company: str | None = None,
|
|
221
|
+
from_date: str | None = None,
|
|
222
|
+
to_date: str | None = None,
|
|
223
|
+
presentation_currency: str | None = None,
|
|
224
|
+
):
|
|
225
|
+
db = get_db()
|
|
226
|
+
return _present(db, _chart_of_accounts(db, company, from_date, to_date),
|
|
227
|
+
company, presentation_currency, to_date)
|
|
228
|
+
|
|
229
|
+
|
|
124
230
|
def _general_ledger(db, filters=None):
|
|
125
231
|
filters = filters or {}
|
|
126
232
|
where = ["is_cancelled = 0"]
|
|
@@ -226,6 +226,11 @@ export const api = {
|
|
|
226
226
|
`/reports/trial-balance${qs(params)}`,
|
|
227
227
|
),
|
|
228
228
|
|
|
229
|
+
chartOfAccounts: (params?: Record<string, string>) =>
|
|
230
|
+
request<{ accounts: any[]; from_date: string | null; to_date: string | null }>(
|
|
231
|
+
`/reports/chart-of-accounts${qs(params)}`,
|
|
232
|
+
),
|
|
233
|
+
|
|
229
234
|
generalLedger: (params?: Record<string, string>) =>
|
|
230
235
|
request<{
|
|
231
236
|
rows: any[];
|
|
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
|