lambda-erp 0.7.0__tar.gz → 0.8.0__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 (88) hide show
  1. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/PKG-INFO +1 -1
  2. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/chat.py +13 -1
  3. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/main.py +2 -1
  4. lambda_erp-0.8.0/api/routers/availability.py +154 -0
  5. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/services.py +4 -1
  6. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/frontend/src/api/client.ts +57 -0
  7. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/pyproject.toml +1 -1
  8. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/.gitignore +0 -0
  9. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/LICENSE +0 -0
  10. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/README.md +0 -0
  11. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/__init__.py +0 -0
  12. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/attachments.py +0 -0
  13. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/auth.py +0 -0
  14. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/bootstrap.py +0 -0
  15. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/demo_limits.py +0 -0
  16. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/deps.py +0 -0
  17. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/errors.py +0 -0
  18. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/oauth.py +0 -0
  19. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/pdf.py +0 -0
  20. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/providers.py +0 -0
  21. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/remarks_md.py +0 -0
  22. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/routers/__init__.py +0 -0
  23. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/routers/accounting.py +0 -0
  24. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/routers/admin.py +0 -0
  25. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/routers/analytics.py +0 -0
  26. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/routers/bank_reconciliation.py +0 -0
  27. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/routers/chat_api.py +0 -0
  28. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/routers/documents.py +0 -0
  29. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/routers/masters.py +0 -0
  30. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/routers/mcp.py +0 -0
  31. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/routers/proposals.py +0 -0
  32. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/routers/reports.py +0 -0
  33. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/routers/setup.py +0 -0
  34. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/templates/document.html +0 -0
  35. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/api/templates/proposal.html +0 -0
  36. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/docs/agents/README.md +0 -0
  37. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/frontend/LICENSE +0 -0
  38. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/frontend/README.md +0 -0
  39. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/__init__.py +0 -0
  40. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/__init__.py +0 -0
  41. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/bank_transaction.py +0 -0
  42. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/budget.py +0 -0
  43. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  44. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/general_ledger.py +0 -0
  45. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/journal_entry.py +0 -0
  46. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/payment_entry.py +0 -0
  47. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/pos_invoice.py +0 -0
  48. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/purchase_invoice.py +0 -0
  49. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/revaluation.py +0 -0
  50. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/sales_invoice.py +0 -0
  51. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/setup/__init__.py +0 -0
  52. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/setup/engine.py +0 -0
  53. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/setup/pack.py +0 -0
  54. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
  55. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/setup/packs/ch.py +0 -0
  56. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
  57. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
  58. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
  59. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/setup/packs/generic.py +0 -0
  60. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/setup/profiles.py +0 -0
  61. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/setup/spine.py +0 -0
  62. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/accounting/subscription.py +0 -0
  63. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/assets/__init__.py +0 -0
  64. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/assets/asset.py +0 -0
  65. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/assets/reservation.py +0 -0
  66. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/buying/__init__.py +0 -0
  67. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/buying/purchase_order.py +0 -0
  68. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/controllers/__init__.py +0 -0
  69. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/controllers/currency.py +0 -0
  70. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/controllers/defaults.py +0 -0
  71. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/controllers/pricing_rule.py +0 -0
  72. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  73. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/database.py +0 -0
  74. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/exceptions.py +0 -0
  75. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/hooks.py +0 -0
  76. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/model.py +0 -0
  77. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/selling/__init__.py +0 -0
  78. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/selling/proposal.py +0 -0
  79. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/selling/quotation.py +0 -0
  80. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/selling/sales_order.py +0 -0
  81. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/simulation.py +0 -0
  82. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/stock/__init__.py +0 -0
  83. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/stock/delivery_note.py +0 -0
  84. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/stock/purchase_receipt.py +0 -0
  85. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/stock/stock_entry.py +0 -0
  86. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/stock/stock_ledger.py +0 -0
  87. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/lambda_erp/utils.py +0 -0
  88. {lambda_erp-0.7.0 → lambda_erp-0.8.0}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lambda-erp
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: Apache-2.0
@@ -467,7 +467,7 @@ TOOLS = [
467
467
  "type": "object",
468
468
  "properties": {
469
469
  "doctype": {"type": "string", "enum": DOCUMENT_SLUGS, "description": "Document type slug"},
470
- "filters": {"type": "object", "description": "Optional filters on any column. Equality: {\"status\": \"Draft\", \"customer\": \"CUST-001\"}. Comparison (value = a 2-item array [op, value]): {\"grand_total\": [\">\", 100]}, {\"fit\": [\"!=\", \"A\"]}. NULL checks (1-item array): {\"fit\": [\"is null\"]}, {\"main_email\": [\"is not null\"]}. Allowed ops: =, !=, >, <, >=, <=, like, not like, is null, is not null.", "default": {}},
470
+ "filters": {"type": "object", "description": "Optional filters on any column. Equality: {\"status\": \"Draft\", \"customer\": \"CUST-001\"}. Comparison (value = a 2-item array [op, value]): {\"grand_total\": [\">\", 100]}, {\"fit\": [\"!=\", \"A\"]}. NULL checks (1-item array): {\"fit\": [\"is null\"]}, {\"main_email\": [\"is not null\"]}. Allowed ops: =, !=, >, <, >=, <=, like, not like, is null, is not null. Free-text search: {\"search\": \"acme\"} matches the doctype's default text columns; narrow it with {\"search\": \"acme\", \"search_fields\": [\"company_name\", \"tags\"]}.", "default": {}},
471
471
  "order_by": {"type": "string", "description": "Optional column to sort by (e.g. \"occurred_at\" for a timeline). Defaults to creation."},
472
472
  "order": {"type": "string", "enum": ["asc", "desc"], "description": "Sort direction (default desc)", "default": "desc"},
473
473
  "limit": {"type": "integer", "description": "Max results (default 20, max 500)", "default": 20},
@@ -1317,6 +1317,18 @@ def _handle_list_documents(args):
1317
1317
  # within the tool-result budget. Use get_document to drill into one doc.
1318
1318
  doctype = args["doctype"]
1319
1319
  filters = args.get("filters") or {}
1320
+ # Chat callers routinely pass `search` but omit `search_fields`; with no fields
1321
+ # the free-text search silently no-ops and returns the newest N by creation
1322
+ # (2026-08-13: tag lookups on leads "found nothing"). Default the columns to the
1323
+ # doctype's registered chat fields so document search matches a record the way
1324
+ # search_masters already does for masters. The frontend never hits this — its
1325
+ # UI config always sends search_fields.
1326
+ if filters.get("search") and not filters.get("search_fields"):
1327
+ registered = (services.CHAT_DOCTYPES.get(doctype) or {}).get("fields") or []
1328
+ valid = set(services.document_columns(doctype) or [])
1329
+ default_fields = [f for f in registered if f in valid]
1330
+ if default_fields:
1331
+ filters = {**filters, "search_fields": default_fields}
1320
1332
  err = _validate_filter_columns(doctype, filters)
1321
1333
  if err:
1322
1334
  return {"error": err}
@@ -19,7 +19,7 @@ from api.auth import router as auth_router, COOKIE_NAME, decode_token
19
19
  from api.oauth import router as oauth_router
20
20
  from api.attachments import router as attachments_router
21
21
  from api.chat import chat_websocket, router as chat_router
22
- from api.routers import admin, documents, masters, reports, setup as setup_router, bank_reconciliation, analytics, accounting, proposals, chat_api, mcp
22
+ from api.routers import admin, documents, masters, reports, setup as setup_router, bank_reconciliation, analytics, accounting, proposals, chat_api, mcp, availability
23
23
 
24
24
 
25
25
  def load_plugins() -> None:
@@ -106,6 +106,7 @@ app.include_router(attachments_router, prefix="/api")
106
106
  app.include_router(documents.router, prefix="/api")
107
107
  app.include_router(proposals.router, prefix="/api")
108
108
  app.include_router(masters.router, prefix="/api")
109
+ app.include_router(availability.router, prefix="/api")
109
110
  app.include_router(reports.router, prefix="/api")
110
111
  app.include_router(analytics.router, prefix="/api")
111
112
  app.include_router(setup_router.router, prefix="/api")
@@ -0,0 +1,154 @@
1
+ """Rental availability endpoint.
2
+
3
+ Exposes the Reservation availability engine (lambda_erp/assets/reservation.py)
4
+ over REST so the booking form can check "is this free?" before saving and the
5
+ fleet calendar can render free vs booked. Read-only — posts nothing.
6
+
7
+ See docs/RENTAL_UI_PLAN.md (Phase 1). The overlap rule itself lives in one
8
+ place (reservation.overlapping_reservations); this router only surfaces it.
9
+ """
10
+
11
+ from fastapi import APIRouter, Depends, Query, HTTPException
12
+
13
+ from lambda_erp.database import get_db
14
+ from lambda_erp.exceptions import ValidationError
15
+ from lambda_erp.assets.reservation import (
16
+ pool_capacity,
17
+ committed_qty,
18
+ available_qty,
19
+ available_assets,
20
+ overlapping_reservations,
21
+ )
22
+ from api.auth import require_role
23
+
24
+ router = APIRouter(
25
+ prefix="/availability",
26
+ tags=["availability"],
27
+ dependencies=[Depends(require_role("viewer"))],
28
+ )
29
+
30
+
31
+ @router.get("")
32
+ def check_availability(
33
+ item_code: str = Query(..., description="Item (machine type) to check."),
34
+ from_datetime: str = Query(..., alias="from", description="Window start (YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)."),
35
+ to_datetime: str = Query(..., alias="to", description="Window end; half-open [from, to)."),
36
+ warehouse: str | None = Query(None, description="Optional yard/warehouse to scope the pool."),
37
+ exclude: str | None = Query(None, description="Reservation name to ignore (editing an existing hire)."),
38
+ ):
39
+ """Availability of an item over a window: total pool, how many are committed,
40
+ how many are free, and which specific units are pickable.
41
+
42
+ - `available_qty` counts free capacity (pooled + pinned commitments both
43
+ reduce it).
44
+ - `available_assets` are the units with no overlapping commitment of their
45
+ own — what dispatch assigns from. A unit can be absent from this list yet
46
+ `available_qty` still be > 0 (a pooled booking reduced the count without
47
+ naming a unit); the form should respect both.
48
+ """
49
+ db = get_db()
50
+ try:
51
+ capacity = pool_capacity(db, item_code, warehouse)
52
+ committed = committed_qty(db, item_code, warehouse, from_datetime, to_datetime, exclude)
53
+ avail_qty = available_qty(db, item_code, warehouse, from_datetime, to_datetime, exclude)
54
+ units = available_assets(db, item_code, warehouse, from_datetime, to_datetime, exclude)
55
+ overlap = overlapping_reservations(
56
+ db, from_dt=from_datetime, to_dt=to_datetime,
57
+ item_code=item_code, warehouse=warehouse, exclude=exclude,
58
+ )
59
+ except ValidationError as e:
60
+ raise HTTPException(status_code=422, detail=str(e))
61
+
62
+ return {
63
+ "item_code": item_code,
64
+ "warehouse": warehouse,
65
+ "from": from_datetime,
66
+ "to": to_datetime,
67
+ "capacity": capacity,
68
+ "committed": committed,
69
+ "available_qty": avail_qty,
70
+ "available": avail_qty > 0,
71
+ "available_assets": [
72
+ {
73
+ "name": a.get("name"),
74
+ "asset_tag": a.get("asset_tag"),
75
+ "warehouse": a.get("warehouse"),
76
+ "status": a.get("status"),
77
+ "meter_reading": a.get("meter_reading"),
78
+ }
79
+ for a in units
80
+ ],
81
+ "overlapping": overlap,
82
+ }
83
+
84
+
85
+ @router.get("/calendar")
86
+ def calendar_feed(
87
+ from_datetime: str = Query(..., alias="from", description="Window start (YYYY-MM-DD or with time)."),
88
+ to_datetime: str = Query(..., alias="to", description="Window end; half-open [from, to)."),
89
+ warehouse: str | None = Query(None, description="Optional yard/warehouse to scope both lanes and bars."),
90
+ item_code: str | None = Query(None, description="Optional item (machine type) to scope to."),
91
+ ):
92
+ """Feed for the fleet availability timeline: the asset lanes (rows) plus the
93
+ reservations that occupy them within the window (bars).
94
+
95
+ - `assets` are the active, non-retired units (optionally scoped by yard /
96
+ item) — the timeline's Y axis.
97
+ - `reservations` are the blocking hires overlapping [from, to) — the bars —
98
+ via the single canonical overlap rule (overlapping_reservations), enriched
99
+ with the party for a bar label. Pooled bookings (no `asset`) carry a null
100
+ `asset`; the caller renders them on an item lane or a "pool" row.
101
+ """
102
+ db = get_db()
103
+
104
+ asset_filters: dict = {"disabled": 0}
105
+ if warehouse:
106
+ asset_filters["warehouse"] = warehouse
107
+ if item_code:
108
+ asset_filters["item_code"] = item_code
109
+ assets = [
110
+ {
111
+ "name": a.get("name"),
112
+ "asset_tag": a.get("asset_tag"),
113
+ "item_code": a.get("item_code"),
114
+ "warehouse": a.get("warehouse"),
115
+ "status": a.get("status"),
116
+ "meter_reading": a.get("meter_reading"),
117
+ }
118
+ for a in db.get_all("Asset", filters=asset_filters, fields=["*"], order_by="item_code, asset_tag")
119
+ if a.get("status") != "Retired"
120
+ ]
121
+
122
+ try:
123
+ rows = overlapping_reservations(
124
+ db, from_dt=from_datetime, to_dt=to_datetime,
125
+ item_code=item_code, warehouse=warehouse,
126
+ )
127
+ except ValidationError as e:
128
+ raise HTTPException(status_code=422, detail=str(e))
129
+
130
+ reservations = []
131
+ for r in rows:
132
+ extra = db.get_value("Reservation", r["name"], ["party_type", "party", "purpose"]) or {}
133
+ reservations.append({
134
+ "name": r.get("name"),
135
+ "item_code": r.get("item_code"),
136
+ "asset": r.get("asset"),
137
+ "warehouse": r.get("warehouse"),
138
+ "qty": r.get("qty"),
139
+ "from_datetime": r.get("from_datetime"),
140
+ "to_datetime": r.get("to_datetime"),
141
+ "status": r.get("status"),
142
+ "party_type": extra.get("party_type"),
143
+ "party": extra.get("party"),
144
+ "purpose": extra.get("purpose"),
145
+ })
146
+
147
+ return {
148
+ "from": from_datetime,
149
+ "to": to_datetime,
150
+ "warehouse": warehouse,
151
+ "item_code": item_code,
152
+ "assets": assets,
153
+ "reservations": reservations,
154
+ }
@@ -483,7 +483,10 @@ def _search_clause(db, doctype: str, doctype_slug: str, search, search_fields):
483
483
  parts, params = [], []
484
484
  for f in (search_fields or []):
485
485
  if f in cols:
486
- parts.append(f'LOWER("{f}") LIKE LOWER(?)')
486
+ # CAST to text so non-text search_fields (e.g. an int/bool column a
487
+ # caller names) don't blow up Postgres with "lower(integer) does not
488
+ # exist" — matches the master path (_handle_search_masters, 2026-08-13).
489
+ parts.append(f'LOWER(CAST("{f}" AS TEXT)) LIKE LOWER(?)')
487
490
  params.append(like)
488
491
  extra = []
489
492
  seen = set()
@@ -70,6 +70,56 @@ function qs(params?: Record<string, string | number | undefined>) {
70
70
  return s ? `?${s}` : "";
71
71
  }
72
72
 
73
+ // --- Rental types (asset availability + fleet calendar feed) ---
74
+ export interface AvailabilityAsset {
75
+ name: string;
76
+ asset_tag: string | null;
77
+ warehouse: string | null;
78
+ status: string | null;
79
+ meter_reading: number | null;
80
+ }
81
+ export interface AvailabilityResult {
82
+ item_code: string;
83
+ warehouse: string | null;
84
+ from: string;
85
+ to: string;
86
+ capacity: number;
87
+ committed: number;
88
+ available_qty: number;
89
+ available: boolean;
90
+ available_assets: AvailabilityAsset[];
91
+ overlapping: unknown[];
92
+ }
93
+ export interface CalendarAsset {
94
+ name: string;
95
+ asset_tag: string | null;
96
+ item_code: string | null;
97
+ warehouse: string | null;
98
+ status: string | null;
99
+ meter_reading: number | null;
100
+ }
101
+ export interface CalendarReservation {
102
+ name: string;
103
+ item_code: string | null;
104
+ asset: string | null;
105
+ warehouse: string | null;
106
+ qty: number | null;
107
+ from_datetime: string;
108
+ to_datetime: string;
109
+ status: string | null;
110
+ party_type: string | null;
111
+ party: string | null;
112
+ purpose: string | null;
113
+ }
114
+ export interface FleetCalendarFeed {
115
+ from: string;
116
+ to: string;
117
+ warehouse: string | null;
118
+ item_code: string | null;
119
+ assets: CalendarAsset[];
120
+ reservations: CalendarReservation[];
121
+ }
122
+
73
123
  export const api = {
74
124
  // Generic request (used by sidebar for chat sessions)
75
125
  request,
@@ -256,6 +306,13 @@ export const api = {
256
306
  stockBalance: (params?: Record<string, string>) =>
257
307
  request<{ rows: any[] }>(`/reports/stock-balance${qs(params)}`),
258
308
 
309
+ // --- Rentals: availability + fleet calendar (docs/RENTAL_UI_PLAN.md) ---
310
+ availability: (params: { item_code: string; from: string; to: string; warehouse?: string; exclude?: string }) =>
311
+ request<AvailabilityResult>(`/availability${qs(params)}`),
312
+
313
+ fleetCalendar: (params: { from: string; to: string; warehouse?: string; item_code?: string }) =>
314
+ request<FleetCalendarFeed>(`/availability/calendar${qs(params)}`),
315
+
259
316
  dashboardSummary: (company?: string) =>
260
317
  request<any>(`/reports/dashboard-summary${qs({ company })}`),
261
318
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.7.0"
3
+ version = "0.8.0"
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