您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 
 
 

757 行
22 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: MIT. See LICENSE
  3. # metadata
  4. '''
  5. Load metadata (DocType) class
  6. Example:
  7. meta = frappe.get_meta('User')
  8. if meta.has_field('first_name'):
  9. print("DocType" table has field "first_name")
  10. '''
  11. import json
  12. import os
  13. from datetime import datetime
  14. import click
  15. import frappe
  16. from frappe import _
  17. from frappe.model import (
  18. child_table_fields,
  19. data_fieldtypes,
  20. default_fields,
  21. no_value_fields,
  22. optional_fields,
  23. table_fields,
  24. )
  25. from frappe.model.base_document import BaseDocument
  26. from frappe.model.document import Document
  27. from frappe.model.workflow import get_workflow_name
  28. from frappe.modules import load_doctype_module
  29. from frappe.utils import cast, cint, cstr
  30. def get_meta(doctype, cached=True):
  31. if cached:
  32. if not frappe.local.meta_cache.get(doctype):
  33. meta = frappe.cache().hget("meta", doctype)
  34. if meta:
  35. meta = Meta(meta)
  36. else:
  37. meta = Meta(doctype)
  38. frappe.cache().hset('meta', doctype, meta.as_dict())
  39. frappe.local.meta_cache[doctype] = meta
  40. return frappe.local.meta_cache[doctype]
  41. else:
  42. return load_meta(doctype)
  43. def load_meta(doctype):
  44. return Meta(doctype)
  45. def get_table_columns(doctype):
  46. return frappe.db.get_table_columns(doctype)
  47. def load_doctype_from_file(doctype):
  48. fname = frappe.scrub(doctype)
  49. with open(frappe.get_app_path("frappe", "core", "doctype", fname, fname + ".json"), "r") as f:
  50. txt = json.loads(f.read())
  51. for d in txt.get("fields", []):
  52. d["doctype"] = "DocField"
  53. for d in txt.get("permissions", []):
  54. d["doctype"] = "DocPerm"
  55. txt["fields"] = [BaseDocument(d) for d in txt["fields"]]
  56. if "permissions" in txt:
  57. txt["permissions"] = [BaseDocument(d) for d in txt["permissions"]]
  58. return txt
  59. class Meta(Document):
  60. _metaclass = True
  61. default_fields = list(default_fields)[1:]
  62. special_doctypes = ("DocField", "DocPerm", "DocType", "Module Def", 'DocType Action', 'DocType Link', 'DocType State')
  63. standard_set_once_fields = [
  64. frappe._dict(fieldname="creation", fieldtype="Datetime"),
  65. frappe._dict(fieldname="owner", fieldtype="Data"),
  66. ]
  67. def __init__(self, doctype):
  68. self._fields = {}
  69. if isinstance(doctype, dict):
  70. super(Meta, self).__init__(doctype)
  71. elif isinstance(doctype, Document):
  72. super(Meta, self).__init__(doctype.as_dict())
  73. self.process()
  74. else:
  75. super(Meta, self).__init__("DocType", doctype)
  76. self.process()
  77. def load_from_db(self):
  78. try:
  79. super(Meta, self).load_from_db()
  80. except frappe.DoesNotExistError:
  81. if self.doctype=="DocType" and self.name in self.special_doctypes:
  82. self.__dict__.update(load_doctype_from_file(self.name))
  83. else:
  84. raise
  85. def process(self):
  86. # don't process for special doctypes
  87. # prevent's circular dependency
  88. if self.name in self.special_doctypes:
  89. return
  90. self.add_custom_fields()
  91. self.apply_property_setters()
  92. self.sort_fields()
  93. self.get_valid_columns()
  94. self.set_custom_permissions()
  95. self.add_custom_links_and_actions()
  96. def as_dict(self, no_nulls = False):
  97. def serialize(doc):
  98. out = {}
  99. for key in doc.__dict__:
  100. value = doc.__dict__.get(key)
  101. if isinstance(value, (list, tuple)):
  102. if len(value) > 0 and hasattr(value[0], '__dict__'):
  103. value = [serialize(d) for d in value]
  104. else:
  105. # non standard list object, skip
  106. continue
  107. if (isinstance(value, (str, int, float, datetime, list, tuple))
  108. or (not no_nulls and value is None)):
  109. out[key] = value
  110. # set empty lists for unset table fields
  111. for table_field in DOCTYPE_TABLE_FIELDS:
  112. if not out.get(table_field.fieldname):
  113. out[table_field.fieldname] = []
  114. return out
  115. return serialize(self)
  116. def get_link_fields(self):
  117. return self.get("fields", {"fieldtype": "Link", "options":["!=", "[Select]"]})
  118. def get_data_fields(self):
  119. return self.get("fields", {"fieldtype": "Data"})
  120. def get_dynamic_link_fields(self):
  121. if not hasattr(self, '_dynamic_link_fields'):
  122. self._dynamic_link_fields = self.get("fields", {"fieldtype": "Dynamic Link"})
  123. return self._dynamic_link_fields
  124. def get_select_fields(self):
  125. return self.get("fields", {"fieldtype": "Select", "options":["not in",
  126. ["[Select]", "Loading..."]]})
  127. def get_image_fields(self):
  128. return self.get("fields", {"fieldtype": "Attach Image"})
  129. def get_code_fields(self):
  130. return self.get("fields", {"fieldtype": "Code"})
  131. def get_set_only_once_fields(self):
  132. '''Return fields with `set_only_once` set'''
  133. if not hasattr(self, "_set_only_once_fields"):
  134. self._set_only_once_fields = self.get("fields", {"set_only_once": 1})
  135. fieldnames = [d.fieldname for d in self._set_only_once_fields]
  136. for df in self.standard_set_once_fields:
  137. if df.fieldname not in fieldnames:
  138. self._set_only_once_fields.append(df)
  139. return self._set_only_once_fields
  140. def get_table_fields(self):
  141. if not hasattr(self, "_table_fields"):
  142. if self.name!="DocType":
  143. self._table_fields = self.get('fields', {"fieldtype": ['in', table_fields]})
  144. else:
  145. self._table_fields = DOCTYPE_TABLE_FIELDS
  146. return self._table_fields
  147. def get_global_search_fields(self):
  148. '''Returns list of fields with `in_global_search` set and `name` if set'''
  149. fields = self.get("fields", {"in_global_search": 1, "fieldtype": ["not in", no_value_fields]})
  150. if getattr(self, 'show_name_in_global_search', None):
  151. fields.append(frappe._dict(fieldtype='Data', fieldname='name', label='Name'))
  152. return fields
  153. def get_valid_columns(self):
  154. if not hasattr(self, "_valid_columns"):
  155. table_exists = frappe.db.table_exists(self.name)
  156. if self.name in self.special_doctypes and table_exists:
  157. self._valid_columns = get_table_columns(self.name)
  158. else:
  159. self._valid_columns = self.default_fields + \
  160. [df.fieldname for df in self.get("fields") if df.fieldtype in data_fieldtypes]
  161. if self.istable:
  162. self._valid_columns += list(child_table_fields)
  163. return self._valid_columns
  164. def get_table_field_doctype(self, fieldname):
  165. return {
  166. "fields": "DocField",
  167. "permissions": "DocPerm",
  168. "actions": "DocType Action",
  169. "links": "DocType Link",
  170. "states": "DocType State",
  171. }.get(fieldname)
  172. def get_field(self, fieldname):
  173. '''Return docfield from meta'''
  174. if not self._fields:
  175. for f in self.get("fields"):
  176. self._fields[f.fieldname] = f
  177. return self._fields.get(fieldname)
  178. def has_field(self, fieldname):
  179. '''Returns True if fieldname exists'''
  180. return True if self.get_field(fieldname) else False
  181. def get_label(self, fieldname):
  182. '''Get label of the given fieldname'''
  183. df = self.get_field(fieldname)
  184. if df:
  185. label = df.label
  186. else:
  187. label = {
  188. 'name': _('ID'),
  189. 'owner': _('Created By'),
  190. 'modified_by': _('Modified By'),
  191. 'creation': _('Created On'),
  192. 'modified': _('Last Modified On'),
  193. '_assign': _('Assigned To')
  194. }.get(fieldname) or _('No Label')
  195. return label
  196. def get_options(self, fieldname):
  197. return self.get_field(fieldname).options
  198. def get_link_doctype(self, fieldname):
  199. df = self.get_field(fieldname)
  200. if df.fieldtype == "Link":
  201. return df.options
  202. elif df.fieldtype == "Dynamic Link":
  203. return self.get_options(df.options)
  204. else:
  205. return None
  206. def get_search_fields(self):
  207. search_fields = self.search_fields or "name"
  208. search_fields = [d.strip() for d in search_fields.split(",")]
  209. if "name" not in search_fields:
  210. search_fields.append("name")
  211. return search_fields
  212. def get_fields_to_fetch(self, link_fieldname=None):
  213. '''Returns a list of docfield objects for fields whose values
  214. are to be fetched and updated for a particular link field
  215. These fields are of type Data, Link, Text, Readonly and their
  216. fetch_from property is set as `link_fieldname`.`source_fieldname`'''
  217. out = []
  218. if not link_fieldname:
  219. link_fields = [df.fieldname for df in self.get_link_fields()]
  220. for df in self.fields:
  221. if df.fieldtype not in no_value_fields and getattr(df, 'fetch_from', None):
  222. if link_fieldname:
  223. if df.fetch_from.startswith(link_fieldname + '.'):
  224. out.append(df)
  225. else:
  226. if '.' in df.fetch_from:
  227. fieldname = df.fetch_from.split('.', 1)[0]
  228. if fieldname in link_fields:
  229. out.append(df)
  230. return out
  231. def get_list_fields(self):
  232. list_fields = ["name"] + [d.fieldname \
  233. for d in self.fields if (d.in_list_view and d.fieldtype in data_fieldtypes)]
  234. if self.title_field and self.title_field not in list_fields:
  235. list_fields.append(self.title_field)
  236. return list_fields
  237. def get_custom_fields(self):
  238. return [d for d in self.fields if d.get('is_custom_field')]
  239. def get_title_field(self):
  240. '''Return the title field of this doctype,
  241. explict via `title_field`, or `title` or `name`'''
  242. title_field = getattr(self, 'title_field', None)
  243. if not title_field and self.has_field('title'):
  244. title_field = 'title'
  245. if not title_field:
  246. title_field = 'name'
  247. return title_field
  248. def get_translatable_fields(self):
  249. '''Return all fields that are translation enabled'''
  250. return [d.fieldname for d in self.fields if d.translatable]
  251. def is_translatable(self, fieldname):
  252. '''Return true of false given a field'''
  253. field = self.get_field(fieldname)
  254. return field and field.translatable
  255. def get_workflow(self):
  256. return get_workflow_name(self.name)
  257. def add_custom_fields(self):
  258. if not frappe.db.table_exists('Custom Field'):
  259. return
  260. custom_fields = frappe.db.sql("""
  261. SELECT * FROM `tabCustom Field`
  262. WHERE dt = %s AND docstatus < 2
  263. """, (self.name,), as_dict=1, update={"is_custom_field": 1})
  264. self.extend("fields", custom_fields)
  265. def apply_property_setters(self):
  266. """
  267. Property Setters are set via Customize Form. They override standard properties
  268. of the doctype or its child properties like fields, links etc. This method
  269. applies the customized properties over the standard meta object
  270. """
  271. if not frappe.db.table_exists('Property Setter'):
  272. return
  273. property_setters = frappe.db.sql("""select * from `tabProperty Setter` where
  274. doc_type=%s""", (self.name,), as_dict=1)
  275. if not property_setters: return
  276. for ps in property_setters:
  277. if ps.doctype_or_field=='DocType':
  278. self.set(ps.property, cast(ps.property_type, ps.value))
  279. elif ps.doctype_or_field=='DocField':
  280. for d in self.fields:
  281. if d.fieldname == ps.field_name:
  282. d.set(ps.property, cast(ps.property_type, ps.value))
  283. break
  284. elif ps.doctype_or_field=='DocType Link':
  285. for d in self.links:
  286. if d.name == ps.row_name:
  287. d.set(ps.property, cast(ps.property_type, ps.value))
  288. break
  289. elif ps.doctype_or_field=='DocType Action':
  290. for d in self.actions:
  291. if d.name == ps.row_name:
  292. d.set(ps.property, cast(ps.property_type, ps.value))
  293. break
  294. elif ps.doctype_or_field=='DocType State':
  295. for d in self.states:
  296. if d.name == ps.row_name:
  297. d.set(ps.property, cast(ps.property_type, ps.value))
  298. break
  299. def add_custom_links_and_actions(self):
  300. for doctype, fieldname in (('DocType Link', 'links'), ('DocType Action', 'actions'), ('DocType State', 'states')):
  301. # ignore_ddl because the `custom` column was added later via a patch
  302. for d in frappe.get_all(doctype, fields='*', filters=dict(parent=self.name, custom=1), ignore_ddl=True):
  303. self.append(fieldname, d)
  304. # set the fields in order if specified
  305. # order is saved as `links_order`
  306. order = json.loads(self.get('{}_order'.format(fieldname)) or '[]')
  307. if order:
  308. name_map = {d.name:d for d in self.get(fieldname)}
  309. new_list = []
  310. for name in order:
  311. if name in name_map:
  312. new_list.append(name_map[name])
  313. # add the missing items that have not be added
  314. # maybe these items were added to the standard product
  315. # after the customization was done
  316. for d in self.get(fieldname):
  317. if d not in new_list:
  318. new_list.append(d)
  319. self.set(fieldname, new_list)
  320. def sort_fields(self):
  321. """sort on basis of insert_after"""
  322. custom_fields = sorted(self.get_custom_fields(), key=lambda df: df.idx)
  323. if custom_fields:
  324. newlist = []
  325. # if custom field is at top
  326. # insert_after is false
  327. for c in list(custom_fields):
  328. if not c.insert_after:
  329. newlist.append(c)
  330. custom_fields.pop(custom_fields.index(c))
  331. # standard fields
  332. newlist += [df for df in self.get('fields') if not df.get('is_custom_field')]
  333. newlist_fieldnames = [df.fieldname for df in newlist]
  334. for i in range(2):
  335. for df in list(custom_fields):
  336. if df.insert_after in newlist_fieldnames:
  337. cf = custom_fields.pop(custom_fields.index(df))
  338. idx = newlist_fieldnames.index(df.insert_after)
  339. newlist.insert(idx + 1, cf)
  340. newlist_fieldnames.insert(idx + 1, cf.fieldname)
  341. if not custom_fields:
  342. break
  343. # worst case, add remaining custom fields to last
  344. if custom_fields:
  345. newlist += custom_fields
  346. # renum idx
  347. for i, f in enumerate(newlist):
  348. f.idx = i + 1
  349. self.fields = newlist
  350. def set_custom_permissions(self):
  351. '''Reset `permissions` with Custom DocPerm if exists'''
  352. if frappe.flags.in_patch or frappe.flags.in_install:
  353. return
  354. if not self.istable and self.name not in ('DocType', 'DocField', 'DocPerm',
  355. 'Custom DocPerm'):
  356. custom_perms = frappe.get_all('Custom DocPerm', fields='*',
  357. filters=dict(parent=self.name), update=dict(doctype='Custom DocPerm'))
  358. if custom_perms:
  359. self.permissions = [Document(d) for d in custom_perms]
  360. def get_fieldnames_with_value(self, with_field_meta=False):
  361. def is_value_field(docfield):
  362. return not (
  363. docfield.get("is_virtual")
  364. or docfield.fieldtype in no_value_fields
  365. )
  366. if with_field_meta:
  367. return [df for df in self.fields if is_value_field(df)]
  368. return [df.fieldname for df in self.fields if is_value_field(df)]
  369. def get_fields_to_check_permissions(self, user_permission_doctypes):
  370. fields = self.get("fields", {
  371. "fieldtype":"Link",
  372. "parent": self.name,
  373. "ignore_user_permissions":("!=", 1),
  374. "options":("in", user_permission_doctypes)
  375. })
  376. if self.name in user_permission_doctypes:
  377. fields.append(frappe._dict({
  378. "label":"Name",
  379. "fieldname":"name",
  380. "options": self.name
  381. }))
  382. return fields
  383. def get_high_permlevel_fields(self):
  384. """Build list of fields with high perm level and all the higher perm levels defined."""
  385. if not hasattr(self, "high_permlevel_fields"):
  386. self.high_permlevel_fields = []
  387. for df in self.fields:
  388. if df.permlevel > 0:
  389. self.high_permlevel_fields.append(df)
  390. return self.high_permlevel_fields
  391. def get_permlevel_access(self, permission_type='read', parenttype=None):
  392. has_access_to = []
  393. roles = frappe.get_roles()
  394. for perm in self.get_permissions(parenttype):
  395. if perm.role in roles and perm.get(permission_type):
  396. if perm.permlevel not in has_access_to:
  397. has_access_to.append(perm.permlevel)
  398. return has_access_to
  399. def get_permissions(self, parenttype=None):
  400. if self.istable and parenttype:
  401. # use parent permissions
  402. permissions = frappe.get_meta(parenttype).permissions
  403. else:
  404. permissions = self.get('permissions', [])
  405. return permissions
  406. def get_dashboard_data(self):
  407. '''Returns dashboard setup related to this doctype.
  408. This method will return the `data` property in the `[doctype]_dashboard.py`
  409. file in the doctype's folder, along with any overrides or extensions
  410. implemented in other Frappe applications via hooks.
  411. '''
  412. data = frappe._dict()
  413. if not self.custom:
  414. try:
  415. module = load_doctype_module(self.name, suffix='_dashboard')
  416. if hasattr(module, 'get_data'):
  417. data = frappe._dict(module.get_data())
  418. except ImportError:
  419. pass
  420. self.add_doctype_links(data)
  421. if not self.custom:
  422. for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []):
  423. data = frappe._dict(frappe.get_attr(hook)(data=data))
  424. return data
  425. def add_doctype_links(self, data):
  426. '''add `links` child table in standard link dashboard format'''
  427. dashboard_links = []
  428. if getattr(self, 'links', None):
  429. dashboard_links.extend(self.links)
  430. if not data.transactions:
  431. # init groups
  432. data.transactions = []
  433. if not data.non_standard_fieldnames:
  434. data.non_standard_fieldnames = {}
  435. if not data.internal_links:
  436. data.internal_links = {}
  437. for link in dashboard_links:
  438. link.added = False
  439. if link.hidden:
  440. continue
  441. for group in data.transactions:
  442. group = frappe._dict(group)
  443. # For internal links parent doctype will be the key
  444. doctype = link.parent_doctype or link.link_doctype
  445. # group found
  446. if link.group and _(group.label) == _(link.group):
  447. if doctype not in group.get('items'):
  448. group.get('items').append(doctype)
  449. link.added = True
  450. if not link.added:
  451. # group not found, make a new group
  452. data.transactions.append(dict(
  453. label = link.group,
  454. items = [link.parent_doctype or link.link_doctype]
  455. ))
  456. if not link.is_child_table:
  457. if link.link_fieldname != data.fieldname:
  458. if data.fieldname:
  459. data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname
  460. else:
  461. data.fieldname = link.link_fieldname
  462. elif link.is_child_table:
  463. if not data.fieldname:
  464. data.fieldname = link.link_fieldname
  465. data.internal_links[link.parent_doctype] = [link.table_fieldname, link.link_fieldname]
  466. def get_row_template(self):
  467. return self.get_web_template(suffix='_row')
  468. def get_list_template(self):
  469. return self.get_web_template(suffix='_list')
  470. def get_web_template(self, suffix=''):
  471. '''Returns the relative path of the row template for this doctype'''
  472. module_name = frappe.scrub(self.module)
  473. doctype = frappe.scrub(self.name)
  474. template_path = frappe.get_module_path(module_name, 'doctype',
  475. doctype, 'templates', doctype + suffix + '.html')
  476. if os.path.exists(template_path):
  477. return '{module_name}/doctype/{doctype_name}/templates/{doctype_name}{suffix}.html'.format(
  478. module_name = module_name, doctype_name = doctype, suffix=suffix)
  479. return None
  480. def is_nested_set(self):
  481. return self.has_field('lft') and self.has_field('rgt')
  482. DOCTYPE_TABLE_FIELDS = [
  483. frappe._dict({"fieldname": "fields", "options": "DocField"}),
  484. frappe._dict({"fieldname": "permissions", "options": "DocPerm"}),
  485. frappe._dict({"fieldname": "actions", "options": "DocType Action"}),
  486. frappe._dict({"fieldname": "links", "options": "DocType Link"}),
  487. frappe._dict({"fieldname": "states", "options": "DocType State"}),
  488. ]
  489. #######
  490. def is_single(doctype):
  491. try:
  492. return frappe.db.get_value("DocType", doctype, "issingle")
  493. except IndexError:
  494. raise Exception('Cannot determine whether %s is single' % doctype)
  495. def get_parent_dt(dt):
  496. parent_dt = frappe.db.get_all('DocField', 'parent', dict(fieldtype=['in', frappe.model.table_fields], options=dt), limit=1)
  497. return parent_dt and parent_dt[0].parent or ''
  498. def set_fieldname(field_id, fieldname):
  499. frappe.db.set_value('DocField', field_id, 'fieldname', fieldname)
  500. def get_field_currency(df, doc=None):
  501. """get currency based on DocField options and fieldvalue in doc"""
  502. currency = None
  503. if not df.get("options"):
  504. return None
  505. if not doc:
  506. return None
  507. if not getattr(frappe.local, "field_currency", None):
  508. frappe.local.field_currency = frappe._dict()
  509. if not (frappe.local.field_currency.get((doc.doctype, doc.name), {}).get(df.fieldname) or
  510. (doc.get("parent") and frappe.local.field_currency.get((doc.doctype, doc.parent), {}).get(df.fieldname))):
  511. ref_docname = doc.get("parent") or doc.name
  512. if ":" in cstr(df.get("options")):
  513. split_opts = df.get("options").split(":")
  514. if len(split_opts)==3 and doc.get(split_opts[1]):
  515. currency = frappe.get_cached_value(split_opts[0], doc.get(split_opts[1]), split_opts[2])
  516. else:
  517. currency = doc.get(df.get("options"))
  518. if doc.get("parenttype"):
  519. if currency:
  520. ref_docname = doc.name
  521. else:
  522. if frappe.get_meta(doc.parenttype).has_field(df.get("options")):
  523. # only get_value if parent has currency field
  524. currency = frappe.db.get_value(doc.parenttype, doc.parent, df.get("options"))
  525. if currency:
  526. frappe.local.field_currency.setdefault((doc.doctype, ref_docname), frappe._dict())\
  527. .setdefault(df.fieldname, currency)
  528. return frappe.local.field_currency.get((doc.doctype, doc.name), {}).get(df.fieldname) or \
  529. (doc.get("parent") and frappe.local.field_currency.get((doc.doctype, doc.parent), {}).get(df.fieldname))
  530. def get_field_precision(df, doc=None, currency=None):
  531. """get precision based on DocField options and fieldvalue in doc"""
  532. from frappe.utils import get_number_format_info
  533. if df.precision:
  534. precision = cint(df.precision)
  535. elif df.fieldtype == "Currency":
  536. precision = cint(frappe.db.get_default("currency_precision"))
  537. if not precision:
  538. number_format = frappe.db.get_default("number_format") or "#,###.##"
  539. decimal_str, comma_str, precision = get_number_format_info(number_format)
  540. else:
  541. precision = cint(frappe.db.get_default("float_precision")) or 3
  542. return precision
  543. def get_default_df(fieldname):
  544. if fieldname in (default_fields + child_table_fields):
  545. if fieldname in ("creation", "modified"):
  546. return frappe._dict(
  547. fieldname = fieldname,
  548. fieldtype = "Datetime"
  549. )
  550. elif fieldname in ("idx", "docstatus"):
  551. return frappe._dict(
  552. fieldname = fieldname,
  553. fieldtype = "Int"
  554. )
  555. return frappe._dict(
  556. fieldname = fieldname,
  557. fieldtype = "Data"
  558. )
  559. def trim_tables(doctype=None, dry_run=False, quiet=False):
  560. """
  561. Removes database fields that don't exist in the doctype (json or custom field). This may be needed
  562. as maintenance since removing a field in a DocType doesn't automatically
  563. delete the db field.
  564. """
  565. UPDATED_TABLES = {}
  566. filters = {"issingle": 0}
  567. if doctype:
  568. filters["name"] = doctype
  569. for doctype in frappe.db.get_all("DocType", filters=filters, pluck="name"):
  570. try:
  571. dropped_columns = trim_table(doctype, dry_run=dry_run)
  572. if dropped_columns:
  573. UPDATED_TABLES[doctype] = dropped_columns
  574. except frappe.db.TableMissingError:
  575. if quiet:
  576. continue
  577. click.secho(f"Ignoring missing table for DocType: {doctype}", fg="yellow", err=True)
  578. click.secho(f"Consider removing record in the DocType table for {doctype}", fg="yellow", err=True)
  579. except Exception as e:
  580. if quiet:
  581. continue
  582. click.echo(e, err=True)
  583. return UPDATED_TABLES
  584. def trim_table(doctype, dry_run=True):
  585. frappe.cache().hdel('table_columns', f"tab{doctype}")
  586. ignore_fields = default_fields + optional_fields + child_table_fields
  587. columns = frappe.db.get_table_columns(doctype)
  588. fields = frappe.get_meta(doctype, cached=False).get_fieldnames_with_value()
  589. is_internal = lambda f: f not in ignore_fields and not f.startswith("_")
  590. columns_to_remove = [
  591. f for f in list(set(columns) - set(fields)) if is_internal(f)
  592. ]
  593. DROPPED_COLUMNS = columns_to_remove[:]
  594. if columns_to_remove and not dry_run:
  595. columns_to_remove = ", ".join(f"DROP `{c}`" for c in columns_to_remove)
  596. frappe.db.sql_ddl(f"ALTER TABLE `tab{doctype}` {columns_to_remove}")
  597. return DROPPED_COLUMNS