lambda-erp 0.8.16__tar.gz → 0.8.18__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.8.16 → lambda_erp-0.8.18}/PKG-INFO +1 -9
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/README.md +0 -8
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/routers/masters.py +139 -49
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/frontend/src/api/client.ts +3 -3
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/pyproject.toml +1 -1
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/.gitignore +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/LICENSE +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/__init__.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/attachments.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/auth.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/bootstrap.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/chat.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/demo_limits.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/deps.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/errors.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/main.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/oauth.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/pdf.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/providers.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/remarks_md.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/routers/__init__.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/routers/accounting.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/routers/actions.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/routers/admin.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/routers/analytics.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/routers/availability.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/routers/documents.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/routers/mcp.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/routers/proposals.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/routers/reports.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/routers/setup.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/services.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/templates/document.html +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/api/templates/proposal.html +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/docs/agents/README.md +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/frontend/LICENSE +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/frontend/README.md +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/assets/__init__.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/assets/asset.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/assets/reservation.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/database.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/model.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.8.16 → lambda_erp-0.8.18}/terraform/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lambda-erp
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.18
|
|
4
4
|
Summary: Core ERP logic - accounting, sales, purchasing, inventory
|
|
5
5
|
Author: TORUS INVESTMENTS AG
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -462,7 +462,6 @@ This is early. The project needs:
|
|
|
462
462
|
- Country compliance packs (tax rules, invoice formats, mandatory fields)
|
|
463
463
|
- Industry templates (services, retail, light manufacturing, SaaS)
|
|
464
464
|
- More preset reports
|
|
465
|
-
- A Postgres storage adapter (the current SQLite layer is fine for local evaluation but will need to be swapped for real multi-user write loads)
|
|
466
465
|
- Better observability around token spend per turn
|
|
467
466
|
- Native messenger integration, for WhatsApp, Telegram, etc.
|
|
468
467
|
|
|
@@ -482,13 +481,6 @@ Release notes live in [CHANGELOG.md](./CHANGELOG.md). Releases are tagged
|
|
|
482
481
|
`vX.Y.Z` and published in lockstep to PyPI (`lambda-erp`) and npm
|
|
483
482
|
(`@lambda-development/erp-core`).
|
|
484
483
|
|
|
485
|
-
---
|
|
486
|
-
|
|
487
|
-
## Status
|
|
488
|
-
|
|
489
|
-
Version 0, and it already implements the vision: invoicing, inventory, accounting, and reporting — all driven by chat. Good for demos, internal tools, and real day-to-day work. It's under active development and improving fast.
|
|
490
|
-
|
|
491
|
-
If you try it, we'd love to know what broke.
|
|
492
484
|
|
|
493
485
|
---
|
|
494
486
|
|
|
@@ -434,7 +434,6 @@ This is early. The project needs:
|
|
|
434
434
|
- Country compliance packs (tax rules, invoice formats, mandatory fields)
|
|
435
435
|
- Industry templates (services, retail, light manufacturing, SaaS)
|
|
436
436
|
- More preset reports
|
|
437
|
-
- A Postgres storage adapter (the current SQLite layer is fine for local evaluation but will need to be swapped for real multi-user write loads)
|
|
438
437
|
- Better observability around token spend per turn
|
|
439
438
|
- Native messenger integration, for WhatsApp, Telegram, etc.
|
|
440
439
|
|
|
@@ -454,13 +453,6 @@ Release notes live in [CHANGELOG.md](./CHANGELOG.md). Releases are tagged
|
|
|
454
453
|
`vX.Y.Z` and published in lockstep to PyPI (`lambda-erp`) and npm
|
|
455
454
|
(`@lambda-development/erp-core`).
|
|
456
455
|
|
|
457
|
-
---
|
|
458
|
-
|
|
459
|
-
## Status
|
|
460
|
-
|
|
461
|
-
Version 0, and it already implements the vision: invoicing, inventory, accounting, and reporting — all driven by chat. Good for demos, internal tools, and real day-to-day work. It's under active development and improving fast.
|
|
462
|
-
|
|
463
|
-
If you try it, we'd love to know what broke.
|
|
464
456
|
|
|
465
457
|
---
|
|
466
458
|
|
|
@@ -303,9 +303,73 @@ def update_master_record(master_type: str, name: str, data: dict) -> dict:
|
|
|
303
303
|
# ad-hoc equality field filter (validated against the master's real columns).
|
|
304
304
|
_MASTER_LIST_RESERVED = {
|
|
305
305
|
"limit", "offset", "include_disabled", "search", "search_fields", "fields",
|
|
306
|
+
"order_by", "order",
|
|
306
307
|
}
|
|
307
308
|
|
|
308
309
|
|
|
310
|
+
def _master_list_where(db, doctype: str, master_type: str, request: Request,
|
|
311
|
+
include_disabled: bool, search: str | None,
|
|
312
|
+
search_fields: str | None) -> tuple[list, list, set]:
|
|
313
|
+
"""Build the validated WHERE clause shared by master list and adjacent.
|
|
314
|
+
|
|
315
|
+
Keeping this in one place is what makes detail prev/next follow the exact
|
|
316
|
+
search and field filters of the list the user opened the record from.
|
|
317
|
+
"""
|
|
318
|
+
columns = db._get_table_columns(doctype)
|
|
319
|
+
where_parts: list = []
|
|
320
|
+
params: list = []
|
|
321
|
+
|
|
322
|
+
active = None if include_disabled else _with_active_filter(db, doctype)
|
|
323
|
+
if active:
|
|
324
|
+
wp, ps = _where_from_filters(active)
|
|
325
|
+
where_parts += wp
|
|
326
|
+
params += ps
|
|
327
|
+
|
|
328
|
+
for key, value in request.query_params.items():
|
|
329
|
+
if key in _MASTER_LIST_RESERVED:
|
|
330
|
+
continue
|
|
331
|
+
if key not in columns:
|
|
332
|
+
raise HTTPException(status_code=400, detail=f"Unknown filter field: {key}")
|
|
333
|
+
where_parts.append(f'"{key}" = ?')
|
|
334
|
+
params.append(value)
|
|
335
|
+
|
|
336
|
+
if search:
|
|
337
|
+
requested = [f.strip() for f in (search_fields or "").split(",") if f.strip()]
|
|
338
|
+
unknown = [f for f in requested if f not in columns]
|
|
339
|
+
if unknown:
|
|
340
|
+
raise HTTPException(status_code=400, detail=f"Unknown search field(s): {', '.join(unknown)}")
|
|
341
|
+
sf = requested or master_search_columns(db, doctype)
|
|
342
|
+
clause, sp = _search_clause(db, doctype, master_type, search, sf)
|
|
343
|
+
if clause:
|
|
344
|
+
where_parts.append(clause)
|
|
345
|
+
params += sp
|
|
346
|
+
|
|
347
|
+
return where_parts, params, columns
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _master_order(columns: set, order_by: str | None, order: str) -> tuple[str, str]:
|
|
351
|
+
if order_by is not None and order_by not in columns:
|
|
352
|
+
raise HTTPException(status_code=400, detail=f"Unknown order_by field: {order_by}")
|
|
353
|
+
direction = order.lower()
|
|
354
|
+
if direction not in ("asc", "desc"):
|
|
355
|
+
raise HTTPException(status_code=400, detail="order must be 'asc' or 'desc'")
|
|
356
|
+
# Reference masters retain their historical default: stable ID order.
|
|
357
|
+
return order_by or "name", direction if order_by else "asc"
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _master_order_sql(column: str, direction: str, *, reverse: bool = False) -> str:
|
|
361
|
+
"""Deterministic master ordering with NULL values consistently at the end."""
|
|
362
|
+
effective = direction
|
|
363
|
+
nulls = "LAST"
|
|
364
|
+
if reverse:
|
|
365
|
+
effective = "desc" if direction == "asc" else "asc"
|
|
366
|
+
nulls = "FIRST"
|
|
367
|
+
sql = f'"{column}" {effective.upper()}'
|
|
368
|
+
if column != "name":
|
|
369
|
+
sql += f" NULLS {nulls}, name {effective.upper()}"
|
|
370
|
+
return sql
|
|
371
|
+
|
|
372
|
+
|
|
309
373
|
@router.get("/{master_type}/filter-values")
|
|
310
374
|
def master_filter_values(
|
|
311
375
|
master_type: str,
|
|
@@ -338,52 +402,22 @@ def list_masters(
|
|
|
338
402
|
search: str | None = None,
|
|
339
403
|
search_fields: str | None = None,
|
|
340
404
|
fields: str | None = None,
|
|
405
|
+
order_by: str | None = None,
|
|
406
|
+
order: str = "asc",
|
|
341
407
|
_user: dict = _viewer,
|
|
342
408
|
):
|
|
343
409
|
doctype, _ = _get_table(master_type)
|
|
344
410
|
if not doctype:
|
|
345
411
|
return {"detail": f"Unknown master type: {master_type}"}
|
|
346
412
|
db = get_db()
|
|
347
|
-
columns =
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
# Active (non-disabled) filter, unless the caller opts in to disabled rows.
|
|
353
|
-
active = None if include_disabled else _with_active_filter(db, doctype)
|
|
354
|
-
if active:
|
|
355
|
-
wp, ps = _where_from_filters(active)
|
|
356
|
-
where_parts += wp
|
|
357
|
-
params += ps
|
|
358
|
-
|
|
359
|
-
# Ad-hoc equality field filters: any remaining query param naming a real
|
|
360
|
-
# column (e.g. /masters/customer?customer_group=Commercial). Unknown field
|
|
361
|
-
# -> 400 (a filter on a missing column is a client bug, never interpolated).
|
|
362
|
-
for key, value in request.query_params.items():
|
|
363
|
-
if key in _MASTER_LIST_RESERVED:
|
|
364
|
-
continue
|
|
365
|
-
if key not in columns:
|
|
366
|
-
raise HTTPException(status_code=400, detail=f"Unknown filter field: {key}")
|
|
367
|
-
where_parts.append(f'"{key}" = ?')
|
|
368
|
-
params.append(value)
|
|
369
|
-
|
|
370
|
-
# Free-text search — defaults to the master's text columns when the caller
|
|
371
|
-
# doesn't name search_fields (so a plain ?search=... just works).
|
|
372
|
-
if search:
|
|
373
|
-
sf = [f.strip() for f in (search_fields or "").split(",")
|
|
374
|
-
if f.strip() and f.strip() in columns]
|
|
375
|
-
if not sf:
|
|
376
|
-
sf = master_search_columns(db, doctype)
|
|
377
|
-
clause, sp = _search_clause(db, doctype, master_type, search, sf)
|
|
378
|
-
if clause:
|
|
379
|
-
where_parts.append(clause)
|
|
380
|
-
params += sp
|
|
413
|
+
where_parts, params, columns = _master_list_where(
|
|
414
|
+
db, doctype, master_type, request, include_disabled, search, search_fields,
|
|
415
|
+
)
|
|
416
|
+
sort_column, sort_direction = _master_order(columns, order_by, order)
|
|
381
417
|
|
|
382
418
|
where = (" WHERE " + " AND ".join(where_parts)) if where_parts else ""
|
|
383
419
|
total = count_query_cached(f'SELECT COUNT(*) as c FROM "{doctype}"{where}', params)
|
|
384
420
|
|
|
385
|
-
# Deterministic order by the `name` PK (already indexed) — required for
|
|
386
|
-
# OFFSET pagination and the /adjacent prev/next contract.
|
|
387
421
|
requested_fields = [f.strip() for f in (fields or "").split(",") if f.strip()]
|
|
388
422
|
if requested_fields:
|
|
389
423
|
unknown = [f for f in requested_fields if f not in columns]
|
|
@@ -394,7 +428,10 @@ def list_masters(
|
|
|
394
428
|
projection = ", ".join(f'"{f}"' for f in requested_fields)
|
|
395
429
|
else:
|
|
396
430
|
projection = "*"
|
|
397
|
-
query =
|
|
431
|
+
query = (
|
|
432
|
+
f'SELECT {projection} FROM "{doctype}"{where} '
|
|
433
|
+
f'ORDER BY {_master_order_sql(sort_column, sort_direction)} LIMIT {int(limit)}'
|
|
434
|
+
)
|
|
398
435
|
if offset:
|
|
399
436
|
query += f" OFFSET {int(offset)}"
|
|
400
437
|
rows = db.sql(query, params)
|
|
@@ -424,26 +461,79 @@ def search_masters(master_type: str, q: str = "", _user: dict = _viewer):
|
|
|
424
461
|
|
|
425
462
|
|
|
426
463
|
@router.get("/{master_type}/{name}/adjacent")
|
|
427
|
-
def adjacent_master(
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
464
|
+
def adjacent_master(
|
|
465
|
+
master_type: str,
|
|
466
|
+
name: str,
|
|
467
|
+
request: Request,
|
|
468
|
+
include_disabled: bool = False,
|
|
469
|
+
search: str | None = None,
|
|
470
|
+
search_fields: str | None = None,
|
|
471
|
+
order_by: str | None = None,
|
|
472
|
+
order: str = "asc",
|
|
473
|
+
_user: dict = _viewer,
|
|
474
|
+
):
|
|
475
|
+
"""Records immediately before/after ``name`` in the current master list.
|
|
476
|
+
|
|
477
|
+
Search, equality filters, disabled visibility, and ordering are shared with
|
|
478
|
+
``list_masters``. Keyset predicates keep navigation proportional to one
|
|
479
|
+
index lookup rather than numbering the entire result set.
|
|
480
|
+
"""
|
|
433
481
|
doctype, _ = _get_table(master_type)
|
|
434
482
|
if not doctype:
|
|
435
483
|
raise HTTPException(status_code=404, detail=f"Unknown master type: {master_type}")
|
|
436
484
|
db = get_db()
|
|
437
|
-
|
|
438
|
-
|
|
485
|
+
where_parts, params, columns = _master_list_where(
|
|
486
|
+
db, doctype, master_type, request, include_disabled, search, search_fields,
|
|
487
|
+
)
|
|
488
|
+
sort_column, sort_direction = _master_order(columns, order_by, order)
|
|
489
|
+
base_where = (" AND ".join(where_parts)) if where_parts else "1 = 1"
|
|
490
|
+
current_rows = db.sql(
|
|
491
|
+
f'SELECT "{sort_column}" AS sort_value FROM "{doctype}" '
|
|
492
|
+
f"WHERE {base_where} AND name = ? LIMIT 1",
|
|
493
|
+
[*params, name],
|
|
494
|
+
)
|
|
495
|
+
if not current_rows:
|
|
496
|
+
return {"prev": None, "next": None}
|
|
497
|
+
current_value = current_rows[0]["sort_value"]
|
|
498
|
+
|
|
499
|
+
def neighbor(toward: str):
|
|
500
|
+
after = toward == "next"
|
|
501
|
+
same_cmp = ">" if sort_direction == "asc" else "<"
|
|
502
|
+
primary_cmp = same_cmp
|
|
503
|
+
if not after:
|
|
504
|
+
same_cmp = "<" if same_cmp == ">" else ">"
|
|
505
|
+
primary_cmp = "<" if primary_cmp == ">" else ">"
|
|
506
|
+
|
|
507
|
+
if sort_column == "name":
|
|
508
|
+
keyset = f'name {same_cmp} ?'
|
|
509
|
+
key_params = [name]
|
|
510
|
+
elif current_value is None:
|
|
511
|
+
if after:
|
|
512
|
+
keyset = f'"{sort_column}" IS NULL AND name {same_cmp} ?'
|
|
513
|
+
else:
|
|
514
|
+
keyset = (
|
|
515
|
+
f'("{sort_column}" IS NOT NULL OR '
|
|
516
|
+
f'("{sort_column}" IS NULL AND name {same_cmp} ?))'
|
|
517
|
+
)
|
|
518
|
+
key_params = [name]
|
|
519
|
+
else:
|
|
520
|
+
keyset = (
|
|
521
|
+
f'("{sort_column}" {primary_cmp} ? OR '
|
|
522
|
+
f'("{sort_column}" = ? AND name {same_cmp} ?)'
|
|
523
|
+
)
|
|
524
|
+
key_params = [current_value, current_value, name]
|
|
525
|
+
if after:
|
|
526
|
+
keyset += f' OR "{sort_column}" IS NULL'
|
|
527
|
+
keyset += ")"
|
|
439
528
|
|
|
440
|
-
def neighbor(cmp: str, direction: str):
|
|
441
529
|
rows = db.sql(
|
|
442
|
-
f'SELECT name FROM "{doctype}" WHERE {
|
|
443
|
-
f
|
|
530
|
+
f'SELECT name FROM "{doctype}" WHERE {base_where} AND {keyset} '
|
|
531
|
+
f'ORDER BY {_master_order_sql(sort_column, sort_direction, reverse=not after)} LIMIT 1',
|
|
532
|
+
[*params, *key_params],
|
|
533
|
+
)
|
|
444
534
|
return rows[0]["name"] if rows else None
|
|
445
535
|
|
|
446
|
-
return {"prev": neighbor("
|
|
536
|
+
return {"prev": neighbor("prev"), "next": neighbor("next")}
|
|
447
537
|
|
|
448
538
|
|
|
449
539
|
@router.get("/{master_type}/{name}")
|
|
@@ -180,10 +180,10 @@ export const api = {
|
|
|
180
180
|
`/documents/${doctype}/${encodeURIComponent(name)}/adjacent${qs(params)}`,
|
|
181
181
|
),
|
|
182
182
|
|
|
183
|
-
// Prev/next master record around `name` in the
|
|
184
|
-
adjacentMaster: (masterType: string, name: string) =>
|
|
183
|
+
// Prev/next master record around `name` in the same order+filters as its list.
|
|
184
|
+
adjacentMaster: (masterType: string, name: string, params?: Record<string, string | number | undefined>) =>
|
|
185
185
|
request<{ prev: string | null; next: string | null }>(
|
|
186
|
-
`/masters/${masterType}/${encodeURIComponent(name)}/adjacent`,
|
|
186
|
+
`/masters/${masterType}/${encodeURIComponent(name)}/adjacent${qs(params)}`,
|
|
187
187
|
),
|
|
188
188
|
|
|
189
189
|
createDocument: (doctype: string, data: 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
|
|
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
|