lambda-erp 0.6.6__tar.gz → 0.6.8__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.6 → lambda_erp-0.6.8}/PKG-INFO +1 -1
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/routers/documents.py +25 -1
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/services.py +101 -11
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/utils.py +10 -3
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/pyproject.toml +1 -1
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/.gitignore +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/LICENSE +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/README.md +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/__init__.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/attachments.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/auth.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/bootstrap.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/chat.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/demo_limits.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/deps.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/errors.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/main.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/oauth.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/pdf.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/providers.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/remarks_md.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/routers/__init__.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/routers/accounting.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/routers/admin.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/routers/analytics.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/routers/masters.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/routers/proposals.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/routers/reports.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/routers/setup.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/templates/document.html +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/api/templates/proposal.html +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/docs/agents/README.md +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/frontend/LICENSE +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/frontend/README.md +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/frontend/src/api/client.ts +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/database.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/model.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.6.6 → lambda_erp-0.6.8}/terraform/README.md +0 -0
|
@@ -28,7 +28,7 @@ _manager = Depends(require_role("manager"))
|
|
|
28
28
|
_LIST_RESERVED = {
|
|
29
29
|
"status", "party", "from_date", "to_date", "docstatus",
|
|
30
30
|
"include_discarded", "limit", "offset", "order_by", "order",
|
|
31
|
-
"date_field",
|
|
31
|
+
"date_field", "search", "search_fields",
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
|
|
@@ -45,6 +45,8 @@ def list_docs(
|
|
|
45
45
|
order_by: str | None = None,
|
|
46
46
|
order: str = "desc",
|
|
47
47
|
date_field: str | None = None,
|
|
48
|
+
search: str | None = None,
|
|
49
|
+
search_fields: str | None = None,
|
|
48
50
|
limit: int = Query(default=50, le=500),
|
|
49
51
|
offset: int = Query(default=0, ge=0),
|
|
50
52
|
_user: dict = _viewer,
|
|
@@ -80,6 +82,18 @@ def list_docs(
|
|
|
80
82
|
if date_field:
|
|
81
83
|
filters["date_field"] = date_field
|
|
82
84
|
|
|
85
|
+
# Free-text search across the doctype's declared search_fields (+ any
|
|
86
|
+
# registered related-table expansion). search_fields is a CSV validated to
|
|
87
|
+
# real columns, mirroring the arbitrary-filter / order_by contract.
|
|
88
|
+
if search:
|
|
89
|
+
filters["search"] = search
|
|
90
|
+
fields = [f.strip() for f in (search_fields or "").split(",") if f.strip()]
|
|
91
|
+
bad = [f for f in fields if f not in columns]
|
|
92
|
+
if bad:
|
|
93
|
+
raise HTTPException(status_code=400, detail=f"Unknown search field(s): {', '.join(bad)}")
|
|
94
|
+
if fields:
|
|
95
|
+
filters["search_fields"] = fields
|
|
96
|
+
|
|
83
97
|
if order_by is not None and order_by not in columns:
|
|
84
98
|
raise HTTPException(status_code=400, detail=f"Unknown order_by field: {order_by}")
|
|
85
99
|
if order.lower() not in ("asc", "desc"):
|
|
@@ -105,6 +119,8 @@ def adjacent_doc(
|
|
|
105
119
|
order_by: str | None = None,
|
|
106
120
|
order: str = "desc",
|
|
107
121
|
date_field: str | None = None,
|
|
122
|
+
search: str | None = None,
|
|
123
|
+
search_fields: str | None = None,
|
|
108
124
|
_user: dict = _viewer,
|
|
109
125
|
):
|
|
110
126
|
"""Prev/next record around `name` in the same order+filters the list uses.
|
|
@@ -130,6 +146,14 @@ def adjacent_doc(
|
|
|
130
146
|
filters[key] = value
|
|
131
147
|
if date_field:
|
|
132
148
|
filters["date_field"] = date_field
|
|
149
|
+
if search:
|
|
150
|
+
filters["search"] = search
|
|
151
|
+
fields = [f.strip() for f in (search_fields or "").split(",") if f.strip()]
|
|
152
|
+
bad = [f for f in fields if f not in columns]
|
|
153
|
+
if bad:
|
|
154
|
+
raise HTTPException(status_code=400, detail=f"Unknown search field(s): {', '.join(bad)}")
|
|
155
|
+
if fields:
|
|
156
|
+
filters["search_fields"] = fields
|
|
133
157
|
if order_by is not None and order_by not in columns:
|
|
134
158
|
raise HTTPException(status_code=400, detail=f"Unknown order_by field: {order_by}")
|
|
135
159
|
if order.lower() not in ("asc", "desc"):
|
|
@@ -403,6 +403,57 @@ def _resolve_date_field(db, doctype: str, override: str | None) -> str | None:
|
|
|
403
403
|
return DATE_FIELDS.get(doctype)
|
|
404
404
|
|
|
405
405
|
|
|
406
|
+
# --- List search seam ---
|
|
407
|
+
#
|
|
408
|
+
# The generic list can free-text search across a doctype's own columns (the
|
|
409
|
+
# frontend passes `search` + the config's `search_fields`). A plugin doctype
|
|
410
|
+
# often also wants to match on a *related* table the core knows nothing about
|
|
411
|
+
# (e.g. find a Lead by one of its Contacts). register_search_expansion lets it:
|
|
412
|
+
# fn(query, db) returns the primary-key names to also include, and the core ORs
|
|
413
|
+
# `name IN (...)` into the search clause. See docs/core-extension-architecture.md.
|
|
414
|
+
|
|
415
|
+
_SEARCH_EXPANSIONS: dict[str, list] = {} # slug -> [fn(query: str, db) -> Iterable[str]]
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def register_search_expansion(doctype_slug: str, fn) -> None:
|
|
419
|
+
"""Register a related-table search for a doctype's list. `fn(query, db)`
|
|
420
|
+
returns an iterable of this doctype's PK names whose related records match
|
|
421
|
+
`query` (e.g. Lead names that have a matching Contact). Those names are
|
|
422
|
+
OR-ed into the list/count/adjacent search alongside the same-table column
|
|
423
|
+
matches. Keep it bounded and indexed — it runs on every search keystroke."""
|
|
424
|
+
_SEARCH_EXPANSIONS.setdefault(doctype_slug, []).append(fn)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _search_clause(db, doctype: str, doctype_slug: str, search, search_fields):
|
|
428
|
+
"""A parenthesized OR-group matching `search` (case-insensitive substring)
|
|
429
|
+
across `search_fields` (kept to real columns) plus any PK names a registered
|
|
430
|
+
search expansion returns. Returns (clause, params), or (None, []) when
|
|
431
|
+
there's nothing to match on. Same shape for list/count/adjacent so prev/next
|
|
432
|
+
tracks the searched list exactly."""
|
|
433
|
+
if not search:
|
|
434
|
+
return None, []
|
|
435
|
+
cols = db._get_table_columns(doctype)
|
|
436
|
+
like = f"%{search}%"
|
|
437
|
+
parts, params = [], []
|
|
438
|
+
for f in (search_fields or []):
|
|
439
|
+
if f in cols:
|
|
440
|
+
parts.append(f'LOWER("{f}") LIKE LOWER(?)')
|
|
441
|
+
params.append(like)
|
|
442
|
+
extra = []
|
|
443
|
+
seen = set()
|
|
444
|
+
for fn in _SEARCH_EXPANSIONS.get(doctype_slug, []):
|
|
445
|
+
for pk in (fn(search, db) or []):
|
|
446
|
+
if pk and pk not in seen:
|
|
447
|
+
seen.add(pk)
|
|
448
|
+
extra.append(pk)
|
|
449
|
+
if extra:
|
|
450
|
+
parts.append(f'name IN ({",".join("?" for _ in extra)})')
|
|
451
|
+
params.extend(extra)
|
|
452
|
+
if not parts:
|
|
453
|
+
return None, []
|
|
454
|
+
return "(" + " OR ".join(parts) + ")", params
|
|
455
|
+
|
|
456
|
+
|
|
406
457
|
def _exclude_discarded(db, doctype: str, db_filters: dict, include_discarded: bool) -> None:
|
|
407
458
|
"""Hide voided drafts (discarded=1) unless explicitly requested. Guarded on
|
|
408
459
|
the column existing so non-submittable doctypes are unaffected."""
|
|
@@ -433,6 +484,8 @@ def list_documents(doctype_slug: str, filters: dict = None, limit: int = 50, off
|
|
|
433
484
|
from_date = None
|
|
434
485
|
to_date = None
|
|
435
486
|
date_field_override = None
|
|
487
|
+
search = None
|
|
488
|
+
search_fields = None
|
|
436
489
|
if filters:
|
|
437
490
|
for key, value in filters.items():
|
|
438
491
|
if value is None or value == "":
|
|
@@ -443,6 +496,10 @@ def list_documents(doctype_slug: str, filters: dict = None, limit: int = 50, off
|
|
|
443
496
|
to_date = value
|
|
444
497
|
elif key == "date_field":
|
|
445
498
|
date_field_override = value
|
|
499
|
+
elif key == "search":
|
|
500
|
+
search = value
|
|
501
|
+
elif key == "search_fields":
|
|
502
|
+
search_fields = value
|
|
446
503
|
else:
|
|
447
504
|
db_filters[key] = value
|
|
448
505
|
|
|
@@ -461,18 +518,23 @@ def list_documents(doctype_slug: str, filters: dict = None, limit: int = 50, off
|
|
|
461
518
|
else:
|
|
462
519
|
db_filters[date_field] = ("<=", to_date)
|
|
463
520
|
|
|
521
|
+
search_where, search_params = _search_clause(db, doctype, doctype_slug, search, search_fields)
|
|
522
|
+
|
|
464
523
|
# If both from and to are set, the dict can only hold one constraint per key
|
|
465
524
|
# Fall through to a raw SQL query for that case
|
|
466
525
|
if date_field and from_date and to_date:
|
|
467
526
|
return _list_with_date_range(
|
|
468
|
-
db, doctype, doctype_slug, db_filters, date_field, from_date, to_date, limit, offset
|
|
527
|
+
db, doctype, doctype_slug, db_filters, date_field, from_date, to_date, limit, offset,
|
|
528
|
+
extra_where=search_where, extra_params=search_params,
|
|
469
529
|
)
|
|
470
530
|
|
|
471
531
|
order_clause = _order_clause(order_by, order)
|
|
472
532
|
|
|
473
|
-
# get_all
|
|
474
|
-
|
|
475
|
-
|
|
533
|
+
# get_all can't express OFFSET or a free-text OR-group, so drop to raw SQL
|
|
534
|
+
# whenever either is in play.
|
|
535
|
+
if offset or search_where:
|
|
536
|
+
return _list_with_offset(db, doctype, doctype_slug, db_filters, limit, offset, order_clause,
|
|
537
|
+
extra_where=search_where, extra_params=search_params)
|
|
476
538
|
|
|
477
539
|
rows = db.get_all(
|
|
478
540
|
doctype,
|
|
@@ -496,6 +558,8 @@ def count_documents(doctype_slug: str, filters: dict = None, include_discarded:
|
|
|
496
558
|
from_date = None
|
|
497
559
|
to_date = None
|
|
498
560
|
date_field_override = None
|
|
561
|
+
search = None
|
|
562
|
+
search_fields = None
|
|
499
563
|
if filters:
|
|
500
564
|
for key, value in filters.items():
|
|
501
565
|
if value is None or value == "":
|
|
@@ -506,6 +570,10 @@ def count_documents(doctype_slug: str, filters: dict = None, include_discarded:
|
|
|
506
570
|
to_date = value
|
|
507
571
|
elif key == "date_field":
|
|
508
572
|
date_field_override = value
|
|
573
|
+
elif key == "search":
|
|
574
|
+
search = value
|
|
575
|
+
elif key == "search_fields":
|
|
576
|
+
search_fields = value
|
|
509
577
|
else:
|
|
510
578
|
db_filters[key] = value
|
|
511
579
|
|
|
@@ -529,6 +597,11 @@ def count_documents(doctype_slug: str, filters: dict = None, include_discarded:
|
|
|
529
597
|
where_parts.append(f'"{k}" = ?')
|
|
530
598
|
params.append(v)
|
|
531
599
|
|
|
600
|
+
search_where, search_params = _search_clause(db, doctype, doctype_slug, search, search_fields)
|
|
601
|
+
if search_where:
|
|
602
|
+
where_parts.append(search_where)
|
|
603
|
+
params.extend(search_params)
|
|
604
|
+
|
|
532
605
|
query = f'SELECT COUNT(*) as c FROM "{doctype}"'
|
|
533
606
|
if where_parts:
|
|
534
607
|
query += " WHERE " + " AND ".join(where_parts)
|
|
@@ -536,12 +609,13 @@ def count_documents(doctype_slug: str, filters: dict = None, include_discarded:
|
|
|
536
609
|
return int(rows[0]["c"]) if rows else 0
|
|
537
610
|
|
|
538
611
|
|
|
539
|
-
def _filter_where(db, doctype: str, filters: dict, include_discarded: bool):
|
|
612
|
+
def _filter_where(db, doctype: str, doctype_slug: str, filters: dict, include_discarded: bool):
|
|
540
613
|
"""Build (where_parts, params) for a doctype from a list-style `filters` dict
|
|
541
|
-
(equality filters + from_date/to_date on the doctype's date field +
|
|
542
|
-
exclusion). Same semantics as list_documents/count_documents
|
|
543
|
-
matches exactly what the list shows."""
|
|
614
|
+
(equality filters + from_date/to_date on the doctype's date field + free-text
|
|
615
|
+
search + discard exclusion). Same semantics as list_documents/count_documents
|
|
616
|
+
so prev/next matches exactly what the list shows."""
|
|
544
617
|
db_filters, from_date, to_date, date_field_override = {}, None, None, None
|
|
618
|
+
search = search_fields = None
|
|
545
619
|
for key, value in (filters or {}).items():
|
|
546
620
|
if value is None or value == "":
|
|
547
621
|
continue
|
|
@@ -551,6 +625,10 @@ def _filter_where(db, doctype: str, filters: dict, include_discarded: bool):
|
|
|
551
625
|
to_date = value
|
|
552
626
|
elif key == "date_field":
|
|
553
627
|
date_field_override = value
|
|
628
|
+
elif key == "search":
|
|
629
|
+
search = value
|
|
630
|
+
elif key == "search_fields":
|
|
631
|
+
search_fields = value
|
|
554
632
|
else:
|
|
555
633
|
db_filters[key] = value
|
|
556
634
|
_exclude_discarded(db, doctype, db_filters, include_discarded)
|
|
@@ -567,6 +645,10 @@ def _filter_where(db, doctype: str, filters: dict, include_discarded: bool):
|
|
|
567
645
|
where_parts.append(f'"{k}" {op} ?'); params.append(val)
|
|
568
646
|
else:
|
|
569
647
|
where_parts.append(f'"{k}" = ?'); params.append(v)
|
|
648
|
+
search_where, search_params = _search_clause(db, doctype, doctype_slug, search, search_fields)
|
|
649
|
+
if search_where:
|
|
650
|
+
where_parts.append(search_where)
|
|
651
|
+
params.extend(search_params)
|
|
570
652
|
return where_parts, params
|
|
571
653
|
|
|
572
654
|
|
|
@@ -591,7 +673,7 @@ def adjacent_documents(doctype_slug: str, name: str, filters: dict = None,
|
|
|
591
673
|
return {"prev": None, "next": None}
|
|
592
674
|
cur_oc = cur[0]["oc"]
|
|
593
675
|
|
|
594
|
-
base_where, base_params = _filter_where(db, doctype, filters or {}, include_discarded)
|
|
676
|
+
base_where, base_params = _filter_where(db, doctype, doctype_slug, filters or {}, include_discarded)
|
|
595
677
|
|
|
596
678
|
def neighbor(direction):
|
|
597
679
|
# DESC list: next = tuple < current, prev = tuple > current (ASC flips).
|
|
@@ -608,7 +690,8 @@ def adjacent_documents(doctype_slug: str, name: str, filters: dict = None,
|
|
|
608
690
|
return {"prev": neighbor("prev"), "next": neighbor("next")}
|
|
609
691
|
|
|
610
692
|
|
|
611
|
-
def _list_with_offset(db, doctype, doctype_slug, db_filters, limit, offset, order_clause="creation DESC"
|
|
693
|
+
def _list_with_offset(db, doctype, doctype_slug, db_filters, limit, offset, order_clause="creation DESC",
|
|
694
|
+
extra_where=None, extra_params=None):
|
|
612
695
|
where_parts = []
|
|
613
696
|
params = []
|
|
614
697
|
for k, v in db_filters.items():
|
|
@@ -619,6 +702,9 @@ def _list_with_offset(db, doctype, doctype_slug, db_filters, limit, offset, orde
|
|
|
619
702
|
else:
|
|
620
703
|
where_parts.append(f'"{k}" = ?')
|
|
621
704
|
params.append(v)
|
|
705
|
+
if extra_where:
|
|
706
|
+
where_parts.append(extra_where)
|
|
707
|
+
params.extend(extra_params or [])
|
|
622
708
|
query = f'SELECT * FROM "{doctype}"'
|
|
623
709
|
if where_parts:
|
|
624
710
|
query += " WHERE " + " AND ".join(where_parts)
|
|
@@ -649,7 +735,8 @@ def _attach_children(db, doctype_slug: str, rows: list) -> list:
|
|
|
649
735
|
return result
|
|
650
736
|
|
|
651
737
|
|
|
652
|
-
def _list_with_date_range(db, doctype, doctype_slug, extra_filters, date_field, from_date, to_date, limit,
|
|
738
|
+
def _list_with_date_range(db, doctype, doctype_slug, extra_filters, date_field, from_date, to_date, limit,
|
|
739
|
+
offset=0, extra_where=None, extra_params=None):
|
|
653
740
|
"""List documents when both from_date and to_date are set."""
|
|
654
741
|
where_parts = [f'"{date_field}" >= ?', f'"{date_field}" <= ?']
|
|
655
742
|
params = [from_date, to_date]
|
|
@@ -661,6 +748,9 @@ def _list_with_date_range(db, doctype, doctype_slug, extra_filters, date_field,
|
|
|
661
748
|
else:
|
|
662
749
|
where_parts.append(f'"{k}" = ?')
|
|
663
750
|
params.append(v)
|
|
751
|
+
if extra_where:
|
|
752
|
+
where_parts.append(extra_where)
|
|
753
|
+
params.extend(extra_params or [])
|
|
664
754
|
query = (
|
|
665
755
|
f'SELECT * FROM "{doctype}" WHERE ' + " AND ".join(where_parts)
|
|
666
756
|
+ " ORDER BY creation DESC"
|
|
@@ -6,7 +6,7 @@ throughout the the reference implementation business logic.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import math
|
|
9
|
-
from datetime import date, datetime, timedelta
|
|
9
|
+
from datetime import date, datetime, timedelta, timezone
|
|
10
10
|
from decimal import Decimal, ROUND_HALF_UP
|
|
11
11
|
|
|
12
12
|
def flt(value, precision=None):
|
|
@@ -54,8 +54,15 @@ def nowdate():
|
|
|
54
54
|
return date.today().isoformat()
|
|
55
55
|
|
|
56
56
|
def now():
|
|
57
|
-
"""Return current
|
|
58
|
-
|
|
57
|
+
"""Return the current instant as a timezone-aware UTC ISO string
|
|
58
|
+
(e.g. "2026-07-29T09:57:00.123456+00:00").
|
|
59
|
+
|
|
60
|
+
The offset is deliberate: a naive isoformat() (no zone) is ambiguous, and a
|
|
61
|
+
client parsing it (JS `new Date(...)`) reads it as *browser-local*, so a
|
|
62
|
+
timestamp stamped on a UTC server renders shifted by the viewer's UTC
|
|
63
|
+
offset. Stamping the zone makes every stored timestamp (creation, modified,
|
|
64
|
+
occurred_at, …) an unambiguous instant that round-trips correctly."""
|
|
65
|
+
return datetime.now(timezone.utc).isoformat()
|
|
59
66
|
|
|
60
67
|
def add_days(dt, days):
|
|
61
68
|
"""Add days to a date."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|