lambda-erp 0.6.3__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.3 → lambda_erp-0.6.4}/PKG-INFO +1 -1
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/routers/documents.py +45 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/services.py +70 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/frontend/src/api/client.ts +6 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/pyproject.toml +1 -1
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/.gitignore +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/LICENSE +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/README.md +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/__init__.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/attachments.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/auth.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/bootstrap.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/chat.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/demo_limits.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/deps.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/errors.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/main.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/oauth.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/pdf.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/providers.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/remarks_md.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/routers/__init__.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/routers/accounting.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/routers/admin.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/routers/analytics.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/routers/masters.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/routers/proposals.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/routers/reports.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/routers/setup.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/templates/document.html +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/api/templates/proposal.html +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/docs/agents/README.md +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/frontend/LICENSE +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/frontend/README.md +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/database.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/model.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.6.3 → lambda_erp-0.6.4}/terraform/README.md +0 -0
|
@@ -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)
|
|
@@ -519,6 +519,76 @@ def count_documents(doctype_slug: str, filters: dict = None, include_discarded:
|
|
|
519
519
|
return int(rows[0]["c"]) if rows else 0
|
|
520
520
|
|
|
521
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
|
+
|
|
522
592
|
def _list_with_offset(db, doctype, doctype_slug, db_filters, limit, offset, order_clause="creation DESC"):
|
|
523
593
|
where_parts = []
|
|
524
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
|
|
File without changes
|
|
File without changes
|