lambda-erp 0.1.24__tar.gz → 0.1.25__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.1.24 → lambda_erp-0.1.25}/PKG-INFO +1 -1
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/chat.py +145 -9
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/database.py +30 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/pyproject.toml +1 -1
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/.gitignore +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/LICENSE +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/README.md +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/__init__.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/attachments.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/auth.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/bootstrap.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/demo_limits.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/deps.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/errors.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/main.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/pdf.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/providers.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/routers/__init__.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/routers/accounting.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/routers/admin.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/routers/analytics.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/routers/documents.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/routers/masters.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/routers/proposals.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/routers/reports.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/routers/setup.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/services.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/templates/document.html +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/api/templates/proposal.html +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/docs/agents/README.md +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/frontend/README.md +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/frontend/src/api/client.ts +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/model.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.1.24 → lambda_erp-0.1.25}/terraform/README.md +0 -0
|
@@ -10,6 +10,7 @@ Supports multiple chat sessions, each with their own history and auto-generated
|
|
|
10
10
|
|
|
11
11
|
import asyncio
|
|
12
12
|
import base64
|
|
13
|
+
import difflib
|
|
13
14
|
import io
|
|
14
15
|
import json
|
|
15
16
|
import logging
|
|
@@ -582,12 +583,31 @@ TOOLS = [
|
|
|
582
583
|
"type": "function",
|
|
583
584
|
"function": {
|
|
584
585
|
"name": "search_masters",
|
|
585
|
-
"description": "Search master data (customers, suppliers, items, warehouses, accounts, companies, cost centers). Returns matching records.",
|
|
586
|
+
"description": "Search master data (customers, suppliers, items, warehouses, accounts, companies, cost centers). Case-insensitive, with fuzzy fallback for misspellings. By default matches across standard text fields (name, display name, address/city/zip); large free-text columns like description/templates are skipped unless named in `fields`. PREFER passing `fields` whenever you know which attribute you're matching on (e.g. a city, an email, a tax id) — it's faster, more precise, and avoids false hits from other columns. Returns matching records.",
|
|
586
587
|
"parameters": {
|
|
587
588
|
"type": "object",
|
|
588
589
|
"properties": {
|
|
589
590
|
"master_type": {"type": "string", "enum": MASTER_TYPES},
|
|
590
591
|
"query": {"type": "string", "description": "Search term (empty string returns all)", "default": ""},
|
|
592
|
+
"fields": {
|
|
593
|
+
"type": "array",
|
|
594
|
+
"items": {"type": "string"},
|
|
595
|
+
"description": "Recommended: the column(s) to search, e.g. [\"city\"] or [\"customer_name\"]. Narrowing here is faster and avoids matching unrelated columns. Omit only when you genuinely don't know which field holds the value, to search all standard text fields.",
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
"required": ["master_type"],
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
"type": "function",
|
|
604
|
+
"function": {
|
|
605
|
+
"name": "get_master_fields",
|
|
606
|
+
"description": "List the available columns of a master type (customer, supplier, item, ...). Call this BEFORE passing `fields` to search_masters (or before guessing column names) so you target real fields instead of guessing. Returns: `fields` (all columns), `default_search_fields` (what search_masters searches when `fields` is omitted), and `bulk_text_fields` (large text columns searched only when named in `fields`).",
|
|
607
|
+
"parameters": {
|
|
608
|
+
"type": "object",
|
|
609
|
+
"properties": {
|
|
610
|
+
"master_type": {"type": "string", "enum": MASTER_TYPES},
|
|
591
611
|
},
|
|
592
612
|
"required": ["master_type"],
|
|
593
613
|
},
|
|
@@ -1077,26 +1097,139 @@ def _handle_convert_document(args):
|
|
|
1077
1097
|
return services.convert_document(args["doctype"], args["name"], args["target_doctype"])
|
|
1078
1098
|
|
|
1079
1099
|
|
|
1100
|
+
# Columns that are technically text but are noise for free-text master search.
|
|
1101
|
+
_MASTER_SEARCH_SKIP_COLUMNS = {
|
|
1102
|
+
"naming_series",
|
|
1103
|
+
"owner",
|
|
1104
|
+
"modified_by",
|
|
1105
|
+
"creation",
|
|
1106
|
+
"modified",
|
|
1107
|
+
"created_at",
|
|
1108
|
+
"updated_at",
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
# Large free-text columns excluded from the DEFAULT search: scanning/fuzzing a
|
|
1112
|
+
# big blob (e.g. an item description or an HTML template) is costly and rarely
|
|
1113
|
+
# how you identify a record. Still searchable on demand via the `fields` arg.
|
|
1114
|
+
_MASTER_BULK_TEXT_COLUMNS = {"description", "notes", "remarks", "terms", "comments"}
|
|
1115
|
+
|
|
1116
|
+
# Minimum difflib similarity for a fuzzy (misspelled) match to be returned.
|
|
1117
|
+
_MASTER_FUZZY_THRESHOLD = 0.6
|
|
1118
|
+
# Bounds for the fuzzy scorer so a single large value can't blow up cost:
|
|
1119
|
+
# skip whole-value scoring past this length, and only score the first N tokens.
|
|
1120
|
+
_FUZZY_MAX_VALUE_LEN = 200
|
|
1121
|
+
_FUZZY_MAX_TOKENS = 16
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def _is_bulk_text_column(col):
|
|
1125
|
+
return (col in _MASTER_BULK_TEXT_COLUMNS
|
|
1126
|
+
or col.endswith("_template") or col.endswith("_html"))
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def _master_search_columns(db, doctype):
|
|
1130
|
+
"""Text columns worth matching a query against by default. Discovered from
|
|
1131
|
+
the live schema (new text fields become searchable automatically), minus
|
|
1132
|
+
audit noise and large free-text/template columns."""
|
|
1133
|
+
cols = db._get_text_columns(doctype) - _MASTER_SEARCH_SKIP_COLUMNS
|
|
1134
|
+
# Deterministic order keeps generated SQL and fuzzy scoring stable.
|
|
1135
|
+
return sorted(c for c in cols if not _is_bulk_text_column(c))
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
def _fuzzy_master_search(db, doctype, search_cols, query, has_disabled, limit):
|
|
1139
|
+
"""Fallback for misspellings: rank active rows by best per-field similarity.
|
|
1140
|
+
|
|
1141
|
+
Substring search misses typos ("Meynex" vs "medynex"), so when it finds
|
|
1142
|
+
nothing we pull a capped candidate set and score it in Python with difflib —
|
|
1143
|
+
portable across SQLite and Postgres, no DB extensions required."""
|
|
1144
|
+
where = "WHERE disabled = 0 " if has_disabled else ""
|
|
1145
|
+
candidates = db.sql(f'SELECT * FROM "{doctype}" {where}LIMIT 1000')
|
|
1146
|
+
|
|
1147
|
+
q = query.lower()
|
|
1148
|
+
scored = []
|
|
1149
|
+
for row in candidates:
|
|
1150
|
+
best = 0.0
|
|
1151
|
+
for col in search_cols:
|
|
1152
|
+
val = row.get(col)
|
|
1153
|
+
if not val:
|
|
1154
|
+
continue
|
|
1155
|
+
val = str(val).lower()
|
|
1156
|
+
# Skip whole-value scoring of big blobs (low ratio anyway, high cost).
|
|
1157
|
+
if len(val) <= _FUZZY_MAX_VALUE_LEN:
|
|
1158
|
+
best = max(best, difflib.SequenceMatcher(None, q, val).ratio())
|
|
1159
|
+
# Also score the first few words so a typo'd single token still
|
|
1160
|
+
# matches a multi-word field (e.g. "medynex" within "medynex ag").
|
|
1161
|
+
for token in val.split()[:_FUZZY_MAX_TOKENS]:
|
|
1162
|
+
best = max(best, difflib.SequenceMatcher(None, q, token).ratio())
|
|
1163
|
+
if best >= _MASTER_FUZZY_THRESHOLD:
|
|
1164
|
+
scored.append((best, row))
|
|
1165
|
+
|
|
1166
|
+
scored.sort(key=lambda pair: pair[0], reverse=True)
|
|
1167
|
+
return [dict(row) for _, row in scored[:limit]]
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
def _handle_get_master_fields(args):
|
|
1171
|
+
db = get_db()
|
|
1172
|
+
master_type = args["master_type"]
|
|
1173
|
+
entry = services.MASTER_TABLES.get(master_type)
|
|
1174
|
+
if not entry:
|
|
1175
|
+
return {"error": f"Unknown master type: {master_type}"}
|
|
1176
|
+
doctype, _ = entry
|
|
1177
|
+
default_search = _master_search_columns(db, doctype)
|
|
1178
|
+
bulk = sorted(c for c in db._get_text_columns(doctype) if _is_bulk_text_column(c))
|
|
1179
|
+
return {
|
|
1180
|
+
"master_type": master_type,
|
|
1181
|
+
"fields": sorted(db._get_table_columns(doctype)),
|
|
1182
|
+
# What search_masters searches when `fields` is omitted.
|
|
1183
|
+
"default_search_fields": default_search,
|
|
1184
|
+
# Large text fields searched ONLY when named in search_masters `fields`.
|
|
1185
|
+
"bulk_text_fields": bulk,
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
|
|
1080
1189
|
def _handle_search_masters(args):
|
|
1081
1190
|
db = get_db()
|
|
1082
1191
|
master_type = args["master_type"]
|
|
1083
|
-
query = args.get("query"
|
|
1192
|
+
query = (args.get("query") or "").strip()
|
|
1084
1193
|
|
|
1085
1194
|
entry = services.MASTER_TABLES.get(master_type)
|
|
1086
1195
|
if not entry:
|
|
1087
1196
|
return {"error": f"Unknown master type: {master_type}"}
|
|
1088
1197
|
|
|
1089
|
-
doctype,
|
|
1090
|
-
|
|
1198
|
+
doctype, _name_field = entry
|
|
1199
|
+
has_disabled = "disabled" in db._get_table_columns(doctype)
|
|
1200
|
+
|
|
1091
1201
|
if not query:
|
|
1092
|
-
filters = {"disabled": 0} if
|
|
1202
|
+
filters = {"disabled": 0} if has_disabled else None
|
|
1093
1203
|
return db.get_all(doctype, filters=filters, fields=["*"], limit=20)
|
|
1094
1204
|
|
|
1205
|
+
# Optional `fields` narrows the search to specific columns — cheaper, more
|
|
1206
|
+
# precise, and the only way to reach bulk columns (description, templates)
|
|
1207
|
+
# that the default search skips. Unknown names are ignored.
|
|
1208
|
+
requested = args.get("fields") or []
|
|
1209
|
+
if requested:
|
|
1210
|
+
valid = db._get_table_columns(doctype)
|
|
1211
|
+
search_cols = sorted(c for c in requested if c in valid)
|
|
1212
|
+
if not search_cols:
|
|
1213
|
+
return {"error": f"None of fields {requested} exist on {master_type}"}
|
|
1214
|
+
else:
|
|
1215
|
+
search_cols = _master_search_columns(db, doctype)
|
|
1216
|
+
|
|
1217
|
+
active_prefix = "disabled = 0 AND " if has_disabled else ""
|
|
1218
|
+
|
|
1219
|
+
# Case-insensitive substring match. lower() on both sides is portable (bare
|
|
1220
|
+
# LIKE is case-insensitive on SQLite but case-sensitive on Postgres, which
|
|
1221
|
+
# silently broke prod search); CAST lets targeted non-text columns match too.
|
|
1222
|
+
where = " OR ".join(f'lower(CAST("{col}" AS TEXT)) LIKE ?' for col in search_cols)
|
|
1223
|
+
pattern = f"%{query.lower()}%"
|
|
1095
1224
|
rows = db.sql(
|
|
1096
|
-
f'SELECT * FROM "{doctype}" WHERE {active_prefix}(
|
|
1097
|
-
[
|
|
1225
|
+
f'SELECT * FROM "{doctype}" WHERE {active_prefix}({where}) LIMIT 20',
|
|
1226
|
+
[pattern] * len(search_cols),
|
|
1098
1227
|
)
|
|
1099
|
-
|
|
1228
|
+
if rows:
|
|
1229
|
+
return [dict(r) for r in rows]
|
|
1230
|
+
|
|
1231
|
+
# Nothing matched literally — try fuzzy matching to catch misspellings.
|
|
1232
|
+
return _fuzzy_master_search(db, doctype, search_cols, query, has_disabled, limit=20)
|
|
1100
1233
|
|
|
1101
1234
|
|
|
1102
1235
|
def _ignored_master_fields(master_type: str, data: dict) -> list[str]:
|
|
@@ -1408,6 +1541,7 @@ TOOL_HANDLERS = {
|
|
|
1408
1541
|
"cancel_document": _handle_cancel_document,
|
|
1409
1542
|
"discard_document": _handle_discard_document,
|
|
1410
1543
|
"convert_document": _handle_convert_document,
|
|
1544
|
+
"get_master_fields": _handle_get_master_fields,
|
|
1411
1545
|
"search_masters": _handle_search_masters,
|
|
1412
1546
|
"create_master": _handle_create_master,
|
|
1413
1547
|
"update_master": _handle_update_master,
|
|
@@ -1726,7 +1860,9 @@ When you fill in `item_code`, `customer`, `supplier`, `warehouse`, `company`, et
|
|
|
1726
1860
|
|
|
1727
1861
|
If the user refers to something by its human name ("bill them 8 hours of project mgmt", "add Redstone to the quote"), resolve the key first:
|
|
1728
1862
|
- `search_masters(master_type="item", q="project management")` → returns `[{{name: "SVC-005", item_name: "Project Management"}}]`. Use `name` as `item_code`.
|
|
1729
|
-
- Same for customers, suppliers, warehouses, etc. — `search_masters`
|
|
1863
|
+
- Same for customers, suppliers, warehouses, etc. — `search_masters` is **case-insensitive** and falls back to **fuzzy matching for misspellings**, so a typo'd name ("Meynex") still resolves. Trust its results instead of concluding "not found" after one narrow try.
|
|
1864
|
+
- **Prefer narrowing with `fields`** whenever you know the attribute: search a customer by city with `fields=["city"]`, by email with `fields=["contact_email"]`, etc. It's faster and avoids false matches from unrelated columns. Omitting `fields` searches all standard text fields (a good fallback when you're unsure where the value lives), but large free-text columns (e.g. item `description`) are only searched when you name them explicitly in `fields`.
|
|
1865
|
+
- **Don't guess column names** — call `get_master_fields(master_type=...)` first to see the real columns (and which are searched by default), then pass the exact names to `search_masters` `fields`. This also tells you which large text fields (like `description`) you must name explicitly to search.
|
|
1730
1866
|
|
|
1731
1867
|
When you list masters back to the user (items on an invoice, customers on a report), include the key in parentheses so follow-ups are unambiguous. Example: "Project Management (SVC-005) — 16 Hour".
|
|
1732
1868
|
|
|
@@ -179,6 +179,7 @@ class Database:
|
|
|
179
179
|
self._is_memory = (db_path == ":memory:")
|
|
180
180
|
self._local = threading.local()
|
|
181
181
|
self._col_cache = {} # doctype -> set(columns); invalidated on ALTER
|
|
182
|
+
self._text_col_cache = {} # doctype -> set(text columns); invalidated on ALTER
|
|
182
183
|
if self._is_memory:
|
|
183
184
|
self._lock = threading.Lock()
|
|
184
185
|
self._shared_conn = self._open_conn()
|
|
@@ -1347,6 +1348,7 @@ class Database:
|
|
|
1347
1348
|
if column not in self._get_table_columns(table):
|
|
1348
1349
|
self.conn.execute(self._ddl(f'ALTER TABLE "{table}" ADD COLUMN {column} {definition}'))
|
|
1349
1350
|
self._col_cache.pop(table, None)
|
|
1351
|
+
self._text_col_cache.pop(table, None)
|
|
1350
1352
|
|
|
1351
1353
|
def _migrate(self):
|
|
1352
1354
|
"""Run each pending migration in order, tracking applied versions."""
|
|
@@ -1517,6 +1519,34 @@ class Database:
|
|
|
1517
1519
|
self._col_cache[doctype] = cols
|
|
1518
1520
|
return cols
|
|
1519
1521
|
|
|
1522
|
+
def _get_text_columns(self, doctype):
|
|
1523
|
+
"""Text/character columns of a table — the ones worth matching a
|
|
1524
|
+
free-text search against. Discovered from the live schema so new
|
|
1525
|
+
columns become searchable automatically. Cached; invalidated on ALTER."""
|
|
1526
|
+
cached = self._text_col_cache.get(doctype)
|
|
1527
|
+
if cached is not None:
|
|
1528
|
+
return cached
|
|
1529
|
+
if self.dialect == "postgres":
|
|
1530
|
+
rows = self.conn.execute(
|
|
1531
|
+
"SELECT column_name, data_type FROM information_schema.columns "
|
|
1532
|
+
"WHERE table_name = ?",
|
|
1533
|
+
[doctype],
|
|
1534
|
+
).fetchall()
|
|
1535
|
+
cols = {
|
|
1536
|
+
row[0]
|
|
1537
|
+
for row in rows
|
|
1538
|
+
if "char" in (row[1] or "").lower() or (row[1] or "").lower() in ("text", "citext")
|
|
1539
|
+
}
|
|
1540
|
+
else:
|
|
1541
|
+
cursor = self.conn.execute(f'PRAGMA table_info("{doctype}")')
|
|
1542
|
+
cols = {
|
|
1543
|
+
row[1]
|
|
1544
|
+
for row in cursor.fetchall()
|
|
1545
|
+
if any(t in (row[2] or "").upper() for t in ("CHAR", "CLOB", "TEXT"))
|
|
1546
|
+
}
|
|
1547
|
+
self._text_col_cache[doctype] = cols
|
|
1548
|
+
return cols
|
|
1549
|
+
|
|
1520
1550
|
def _select_fields(self, doctype, fields):
|
|
1521
1551
|
"""Build the SELECT column list, selecting only columns that exist and
|
|
1522
1552
|
padding the rest as NULL. SQLite silently returns an unknown quoted
|
|
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
|