lambda-erp 0.6.7__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.
Files changed (83) hide show
  1. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/PKG-INFO +1 -1
  2. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/routers/documents.py +25 -1
  3. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/services.py +101 -11
  4. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/pyproject.toml +1 -1
  5. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/.gitignore +0 -0
  6. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/LICENSE +0 -0
  7. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/README.md +0 -0
  8. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/__init__.py +0 -0
  9. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/attachments.py +0 -0
  10. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/auth.py +0 -0
  11. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/bootstrap.py +0 -0
  12. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/chat.py +0 -0
  13. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/demo_limits.py +0 -0
  14. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/deps.py +0 -0
  15. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/errors.py +0 -0
  16. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/main.py +0 -0
  17. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/oauth.py +0 -0
  18. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/pdf.py +0 -0
  19. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/providers.py +0 -0
  20. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/remarks_md.py +0 -0
  21. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/routers/__init__.py +0 -0
  22. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/routers/accounting.py +0 -0
  23. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/routers/admin.py +0 -0
  24. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/routers/analytics.py +0 -0
  25. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/routers/bank_reconciliation.py +0 -0
  26. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/routers/chat_api.py +0 -0
  27. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/routers/masters.py +0 -0
  28. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/routers/proposals.py +0 -0
  29. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/routers/reports.py +0 -0
  30. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/routers/setup.py +0 -0
  31. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/templates/document.html +0 -0
  32. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/api/templates/proposal.html +0 -0
  33. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/docs/agents/README.md +0 -0
  34. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/frontend/LICENSE +0 -0
  35. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/frontend/README.md +0 -0
  36. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/frontend/src/api/client.ts +0 -0
  37. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/__init__.py +0 -0
  38. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/__init__.py +0 -0
  39. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/bank_transaction.py +0 -0
  40. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/budget.py +0 -0
  41. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  42. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/general_ledger.py +0 -0
  43. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/journal_entry.py +0 -0
  44. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/payment_entry.py +0 -0
  45. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/pos_invoice.py +0 -0
  46. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/purchase_invoice.py +0 -0
  47. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/revaluation.py +0 -0
  48. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/sales_invoice.py +0 -0
  49. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/__init__.py +0 -0
  50. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/engine.py +0 -0
  51. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/pack.py +0 -0
  52. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
  53. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/packs/ch.py +0 -0
  54. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
  55. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
  56. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
  57. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/packs/generic.py +0 -0
  58. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/profiles.py +0 -0
  59. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/setup/spine.py +0 -0
  60. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/accounting/subscription.py +0 -0
  61. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/buying/__init__.py +0 -0
  62. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/buying/purchase_order.py +0 -0
  63. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/controllers/__init__.py +0 -0
  64. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/controllers/currency.py +0 -0
  65. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/controllers/defaults.py +0 -0
  66. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/controllers/pricing_rule.py +0 -0
  67. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  68. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/database.py +0 -0
  69. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/exceptions.py +0 -0
  70. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/hooks.py +0 -0
  71. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/model.py +0 -0
  72. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/selling/__init__.py +0 -0
  73. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/selling/proposal.py +0 -0
  74. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/selling/quotation.py +0 -0
  75. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/selling/sales_order.py +0 -0
  76. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/simulation.py +0 -0
  77. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/stock/__init__.py +0 -0
  78. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/stock/delivery_note.py +0 -0
  79. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/stock/purchase_receipt.py +0 -0
  80. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/stock/stock_entry.py +0 -0
  81. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/stock/stock_ledger.py +0 -0
  82. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/lambda_erp/utils.py +0 -0
  83. {lambda_erp-0.6.7 → lambda_erp-0.6.8}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-erp
3
- Version: 0.6.7
3
+ Version: 0.6.8
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: Apache-2.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 doesn't support offset, so use raw SQL when needed
474
- if offset:
475
- return _list_with_offset(db, doctype, doctype_slug, db_filters, limit, offset, order_clause)
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 + discard
542
- exclusion). Same semantics as list_documents/count_documents so prev/next
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, offset=0):
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"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.6.7"
3
+ version = "0.6.8"
4
4
  description = "Core ERP logic - accounting, sales, purchasing, inventory"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes