lambda-erp 0.6.2__tar.gz → 0.6.4__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.6.2 → lambda_erp-0.6.4}/PKG-INFO +1 -1
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/chat.py +10 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/main.py +21 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/routers/documents.py +45 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/services.py +109 -3
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/frontend/src/api/client.ts +6 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/pyproject.toml +1 -1
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/.gitignore +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/LICENSE +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/README.md +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/__init__.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/attachments.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/auth.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/bootstrap.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/demo_limits.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/deps.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/errors.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/oauth.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/pdf.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/providers.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/remarks_md.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/routers/__init__.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/routers/accounting.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/routers/admin.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/routers/analytics.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/routers/masters.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/routers/proposals.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/routers/reports.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/routers/setup.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/templates/document.html +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/api/templates/proposal.html +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/docs/agents/README.md +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/frontend/LICENSE +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/frontend/README.md +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/database.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/model.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.6.2 → lambda_erp-0.6.4}/terraform/README.md +0 -0
|
@@ -1198,6 +1198,16 @@ def _chat_doctype_section() -> str:
|
|
|
1198
1198
|
if links:
|
|
1199
1199
|
link_str = ", ".join(f"`{f}` → the {t}'s name" for f, t in links.items())
|
|
1200
1200
|
line += f" Links: {link_str}."
|
|
1201
|
+
# How to open a record of this type (so links resolve).
|
|
1202
|
+
page = services.chat_doctype_page_info(slug)
|
|
1203
|
+
if page and page["kind"] == "self":
|
|
1204
|
+
line += f" Open a record at `/app/{slug}/<name>`."
|
|
1205
|
+
elif page and page["kind"] == "via":
|
|
1206
|
+
line += (f" It has NO page of its own — open it via its parent: "
|
|
1207
|
+
f"`/app/{page['parent_slug']}/<{page['link_field']}>`; "
|
|
1208
|
+
f"never link `/app/{slug}/…`.")
|
|
1209
|
+
elif page and page["kind"] == "none":
|
|
1210
|
+
line += " It has no page — do not emit a link for it."
|
|
1201
1211
|
lines.append(line)
|
|
1202
1212
|
return (
|
|
1203
1213
|
"\n\n## Custom record types (deployment-specific)\n"
|
|
@@ -121,6 +121,27 @@ def health():
|
|
|
121
121
|
return {"status": "ok", "version": get_app_version()}
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
@app.get("/api/chat-doctypes")
|
|
125
|
+
def chat_doctypes():
|
|
126
|
+
"""Public metadata for doctypes registered via register_chat_doctype: their
|
|
127
|
+
description and, for the frontend, how each is opened (`page`). The frontend
|
|
128
|
+
uses the `page` of kind "via" to auto-register a redirect from the doctype's
|
|
129
|
+
(chat-generated) `/app/{slug}/{name}` link to its parent's page. Not
|
|
130
|
+
sensitive — it's the same info the chat system prompt carries."""
|
|
131
|
+
from api import services
|
|
132
|
+
out = []
|
|
133
|
+
for slug, meta in services.CHAT_DOCTYPES.items():
|
|
134
|
+
if slug not in services.SLUG_TO_DOCTYPE:
|
|
135
|
+
continue
|
|
136
|
+
out.append({
|
|
137
|
+
"slug": slug,
|
|
138
|
+
"description": meta.get("description"),
|
|
139
|
+
"fields": meta.get("fields", []),
|
|
140
|
+
"page": services.chat_doctype_page_info(slug),
|
|
141
|
+
})
|
|
142
|
+
return {"doctypes": out}
|
|
143
|
+
|
|
144
|
+
|
|
124
145
|
@app.websocket("/ws/chat")
|
|
125
146
|
async def ws_chat(websocket: WebSocket):
|
|
126
147
|
# Authenticate via cookie, fall back to public_manager for demo mode
|
|
@@ -13,6 +13,7 @@ from api.services import (
|
|
|
13
13
|
list_documents,
|
|
14
14
|
count_documents,
|
|
15
15
|
document_columns,
|
|
16
|
+
adjacent_documents,
|
|
16
17
|
)
|
|
17
18
|
from api.pdf import generate_pdf
|
|
18
19
|
from api.auth import require_role
|
|
@@ -80,6 +81,50 @@ def list_docs(
|
|
|
80
81
|
return {"rows": rows, "total": total, "limit": limit, "offset": offset}
|
|
81
82
|
|
|
82
83
|
|
|
84
|
+
@router.get("/{doctype_slug}/{name}/adjacent")
|
|
85
|
+
def adjacent_doc(
|
|
86
|
+
doctype_slug: str,
|
|
87
|
+
name: str,
|
|
88
|
+
request: Request,
|
|
89
|
+
status: str | None = None,
|
|
90
|
+
party: str | None = None,
|
|
91
|
+
from_date: str | None = None,
|
|
92
|
+
to_date: str | None = None,
|
|
93
|
+
docstatus: int | None = None,
|
|
94
|
+
include_discarded: bool = False,
|
|
95
|
+
order_by: str | None = None,
|
|
96
|
+
order: str = "desc",
|
|
97
|
+
_user: dict = _viewer,
|
|
98
|
+
):
|
|
99
|
+
"""Prev/next record around `name` in the same order+filters the list uses.
|
|
100
|
+
Returns {"prev": name|None, "next": name|None}. Filters mirror the list
|
|
101
|
+
endpoint so navigation stays within the list the user came from."""
|
|
102
|
+
filters = {}
|
|
103
|
+
if status:
|
|
104
|
+
filters["status"] = status
|
|
105
|
+
if docstatus is not None:
|
|
106
|
+
filters["docstatus"] = docstatus
|
|
107
|
+
if party:
|
|
108
|
+
filters["customer"] = party
|
|
109
|
+
if from_date:
|
|
110
|
+
filters["from_date"] = from_date
|
|
111
|
+
if to_date:
|
|
112
|
+
filters["to_date"] = to_date
|
|
113
|
+
columns = document_columns(doctype_slug)
|
|
114
|
+
for key, value in request.query_params.items():
|
|
115
|
+
if key in _LIST_RESERVED:
|
|
116
|
+
continue
|
|
117
|
+
if key not in columns:
|
|
118
|
+
raise HTTPException(status_code=400, detail=f"Unknown filter field: {key}")
|
|
119
|
+
filters[key] = value
|
|
120
|
+
if order_by is not None and order_by not in columns:
|
|
121
|
+
raise HTTPException(status_code=400, detail=f"Unknown order_by field: {order_by}")
|
|
122
|
+
if order.lower() not in ("asc", "desc"):
|
|
123
|
+
raise HTTPException(status_code=400, detail="order must be 'asc' or 'desc'")
|
|
124
|
+
return adjacent_documents(doctype_slug, name, filters=filters,
|
|
125
|
+
include_discarded=include_discarded, order_by=order_by, order=order)
|
|
126
|
+
|
|
127
|
+
|
|
83
128
|
@router.get("/{doctype_slug}/search")
|
|
84
129
|
def search_docs(doctype_slug: str, q: str = "", limit: int = Query(default=10, le=50), _user: dict = _viewer):
|
|
85
130
|
docs = list_documents(doctype_slug, limit=limit)
|
|
@@ -225,12 +225,48 @@ def register_master(slug: str, table: str, name_field: str, *,
|
|
|
225
225
|
CHAT_DOCTYPES: dict = {}
|
|
226
226
|
|
|
227
227
|
|
|
228
|
-
def register_chat_doctype(slug: str, *, description: str, fields: list | None = None
|
|
228
|
+
def register_chat_doctype(slug: str, *, description: str, fields: list | None = None,
|
|
229
|
+
page: str | None = "self") -> None:
|
|
229
230
|
"""Give the AI chat a description (and optional key-field hints) for a doctype
|
|
230
231
|
already registered via `register_doctype`, so build_system_prompt can tell the
|
|
231
232
|
model what it is and how it links (LINK_FIELDS are surfaced automatically).
|
|
232
|
-
No new tools — the document tools already cover every registered doctype.
|
|
233
|
-
|
|
233
|
+
No new tools — the document tools already cover every registered doctype.
|
|
234
|
+
|
|
235
|
+
`page` tells the chat how a record of this doctype is *opened*, so it emits
|
|
236
|
+
correct view links (and the frontend can auto-register redirects):
|
|
237
|
+
"self" (default) — its own page at `/app/{slug}/{name}`;
|
|
238
|
+
"<link_field>" — no page of its own; it's shown inside the parent that
|
|
239
|
+
`link_field` (a LINK_FIELD) references, so it opens at
|
|
240
|
+
`/app/{parent-slug}/{value of link_field}` (e.g. a
|
|
241
|
+
contact opens via its lead: page="lead_id");
|
|
242
|
+
None — no page and no view link at all.
|
|
243
|
+
"""
|
|
244
|
+
CHAT_DOCTYPES[slug] = {"description": description, "fields": fields or [], "page": page}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def chat_doctype_page_info(slug: str) -> dict | None:
|
|
248
|
+
"""Resolve a chat-doctype's `page` config into a link rule, for the system
|
|
249
|
+
prompt and the /api/chat-doctypes read. Returns one of:
|
|
250
|
+
{"kind": "self", "slug": slug}
|
|
251
|
+
{"kind": "via", "link_field": f, "parent_slug": p} # opens via a parent
|
|
252
|
+
{"kind": "none"}
|
|
253
|
+
or None if the slug isn't a registered chat-doctype."""
|
|
254
|
+
meta = CHAT_DOCTYPES.get(slug)
|
|
255
|
+
if not meta:
|
|
256
|
+
return None
|
|
257
|
+
page = meta.get("page", "self")
|
|
258
|
+
if page == "self":
|
|
259
|
+
return {"kind": "self", "slug": slug}
|
|
260
|
+
if page is None:
|
|
261
|
+
return {"kind": "none"}
|
|
262
|
+
# page names a LINK_FIELD -> resolve the parent doctype -> its slug.
|
|
263
|
+
doctype = SLUG_TO_DOCTYPE.get(slug)
|
|
264
|
+
cls = DOCUMENT_CLASSES.get(doctype) if doctype else None
|
|
265
|
+
links = getattr(cls, "LINK_FIELDS", None) or {}
|
|
266
|
+
parent_slug = DOCTYPE_TO_SLUG.get(links.get(page)) if links.get(page) else None
|
|
267
|
+
if not parent_slug:
|
|
268
|
+
return {"kind": "self", "slug": slug} # misconfigured -> safe fallback
|
|
269
|
+
return {"kind": "via", "link_field": page, "parent_slug": parent_slug}
|
|
234
270
|
|
|
235
271
|
|
|
236
272
|
def get_document_class(doctype_slug: str):
|
|
@@ -483,6 +519,76 @@ def count_documents(doctype_slug: str, filters: dict = None, include_discarded:
|
|
|
483
519
|
return int(rows[0]["c"]) if rows else 0
|
|
484
520
|
|
|
485
521
|
|
|
522
|
+
def _filter_where(db, doctype: str, filters: dict, include_discarded: bool):
|
|
523
|
+
"""Build (where_parts, params) for a doctype from a list-style `filters` dict
|
|
524
|
+
(equality filters + from_date/to_date on the doctype's date field + discard
|
|
525
|
+
exclusion). Same semantics as list_documents/count_documents so prev/next
|
|
526
|
+
matches exactly what the list shows."""
|
|
527
|
+
db_filters, from_date, to_date = {}, None, None
|
|
528
|
+
for key, value in (filters or {}).items():
|
|
529
|
+
if value is None or value == "":
|
|
530
|
+
continue
|
|
531
|
+
if key == "from_date":
|
|
532
|
+
from_date = value
|
|
533
|
+
elif key == "to_date":
|
|
534
|
+
to_date = value
|
|
535
|
+
else:
|
|
536
|
+
db_filters[key] = value
|
|
537
|
+
_exclude_discarded(db, doctype, db_filters, include_discarded)
|
|
538
|
+
|
|
539
|
+
date_field = DATE_FIELDS.get(doctype)
|
|
540
|
+
where_parts, params = [], []
|
|
541
|
+
if date_field and from_date:
|
|
542
|
+
where_parts.append(f'"{date_field}" >= ?'); params.append(from_date)
|
|
543
|
+
if date_field and to_date:
|
|
544
|
+
where_parts.append(f'"{date_field}" <= ?'); params.append(to_date)
|
|
545
|
+
for k, v in db_filters.items():
|
|
546
|
+
if isinstance(v, (list, tuple)) and len(v) == 2:
|
|
547
|
+
op, val = v
|
|
548
|
+
where_parts.append(f'"{k}" {op} ?'); params.append(val)
|
|
549
|
+
else:
|
|
550
|
+
where_parts.append(f'"{k}" = ?'); params.append(v)
|
|
551
|
+
return where_parts, params
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def adjacent_documents(doctype_slug: str, name: str, filters: dict = None,
|
|
555
|
+
include_discarded: bool = False, order_by: str = None,
|
|
556
|
+
order: str = "desc") -> dict:
|
|
557
|
+
"""The records immediately before/after `name` in the same order+filters the
|
|
558
|
+
list uses — {"prev": name|None, "next": name|None}. `prev` is one step *up*
|
|
559
|
+
the list (toward the top), `next` one step *down*. Keyset queries (indexed,
|
|
560
|
+
not a full scan). Order defaults to creation DESC (the list default), with
|
|
561
|
+
`name` as a stable tie-break so rows sharing an order value still step 1:1."""
|
|
562
|
+
doctype = SLUG_TO_DOCTYPE.get(doctype_slug)
|
|
563
|
+
if not doctype:
|
|
564
|
+
raise ValueError(f"Unknown document type: {doctype_slug}")
|
|
565
|
+
db = get_db()
|
|
566
|
+
cols = db._get_table_columns(doctype)
|
|
567
|
+
oc = order_by if (order_by and order_by in cols) else "creation"
|
|
568
|
+
desc = str(order).lower() != "asc"
|
|
569
|
+
|
|
570
|
+
cur = db.sql(f'SELECT "{oc}" AS oc FROM "{doctype}" WHERE name = ?', [name])
|
|
571
|
+
if not cur:
|
|
572
|
+
return {"prev": None, "next": None}
|
|
573
|
+
cur_oc = cur[0]["oc"]
|
|
574
|
+
|
|
575
|
+
base_where, base_params = _filter_where(db, doctype, filters or {}, include_discarded)
|
|
576
|
+
|
|
577
|
+
def neighbor(direction):
|
|
578
|
+
# DESC list: next = tuple < current, prev = tuple > current (ASC flips).
|
|
579
|
+
going_less = (direction == "next") == desc
|
|
580
|
+
cmp = "<" if going_less else ">"
|
|
581
|
+
dir_sql = "DESC" if going_less else "ASC"
|
|
582
|
+
keyset = f'(("{oc}" {cmp} ?) OR ("{oc}" = ? AND name {cmp} ?))'
|
|
583
|
+
query = (f'SELECT name FROM "{doctype}" WHERE '
|
|
584
|
+
+ " AND ".join(base_where + [keyset])
|
|
585
|
+
+ f' ORDER BY "{oc}" {dir_sql}, name {dir_sql} LIMIT 1')
|
|
586
|
+
rows = db.sql(query, base_params + [cur_oc, cur_oc, name])
|
|
587
|
+
return rows[0]["name"] if rows else None
|
|
588
|
+
|
|
589
|
+
return {"prev": neighbor("prev"), "next": neighbor("next")}
|
|
590
|
+
|
|
591
|
+
|
|
486
592
|
def _list_with_offset(db, doctype, doctype_slug, db_filters, limit, offset, order_clause="creation DESC"):
|
|
487
593
|
where_parts = []
|
|
488
594
|
params = []
|
|
@@ -123,6 +123,12 @@ export const api = {
|
|
|
123
123
|
getDocument: (doctype: string, name: string) =>
|
|
124
124
|
request<any>(`/documents/${doctype}/${encodeURIComponent(name)}`),
|
|
125
125
|
|
|
126
|
+
// Prev/next record around `name` in the same order+filters the list uses.
|
|
127
|
+
adjacentDocument: (doctype: string, name: string, params?: Record<string, string | number | undefined>) =>
|
|
128
|
+
request<{ prev: string | null; next: string | null }>(
|
|
129
|
+
`/documents/${doctype}/${encodeURIComponent(name)}/adjacent${qs(params)}`,
|
|
130
|
+
),
|
|
131
|
+
|
|
126
132
|
createDocument: (doctype: string, data: any) =>
|
|
127
133
|
request<any>(`/documents/${doctype}`, { method: "POST", body: JSON.stringify(data) }),
|
|
128
134
|
|
|
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
|
|
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
|