New Control: Table MultiSelect (#6675)
* feat(control): Add Table MultiSelect control * fix: Use btn-group instead of span * fix: Remove functionality * fix: Add 'Table MultiSelect' to Field doctypes * fix: Replace usage of string 'Table' with array `table_fields` * fix: Use internal array to store values instead of building from HTML elements * fix(style): Add semicolon * fix: Read only mode and click to navigate to form * style: indent * fix: fallback to empty array * fix: Add formatters in js and py * style: missing semicolon * fix: Add docfield validation
This commit is contained in:
parent
1bf85da159
commit
3b86f16b73
36 changed files with 3040 additions and 2826 deletions
|
|
@ -70,7 +70,8 @@ def clear_doctype_cache(doctype=None):
|
|||
|
||||
# clear all parent doctypes
|
||||
|
||||
for dt in frappe.db.get_all('DocField', 'parent', dict(fieldtype='Table', options=doctype)):
|
||||
for dt in frappe.db.get_all('DocField', 'parent',
|
||||
dict(fieldtype=['in', frappe.model.table_fields], options=doctype)):
|
||||
clear_single(dt.parent)
|
||||
|
||||
# clear all notifications
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -10,7 +10,7 @@ import frappe
|
|||
from frappe import _
|
||||
|
||||
from frappe.utils import now, cint
|
||||
from frappe.model import no_value_fields, default_fields, data_fieldtypes
|
||||
from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields
|
||||
from frappe.model.document import Document
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.desk.notifications import delete_notification_count_for
|
||||
|
|
@ -82,7 +82,7 @@ class DocType(Document):
|
|||
if not [d.fieldname for d in self.fields if d.in_list_view]:
|
||||
cnt = 0
|
||||
for d in self.fields:
|
||||
if d.reqd and not d.hidden and not d.fieldtype == "Table":
|
||||
if d.reqd and not d.hidden and not d.fieldtype in table_fields:
|
||||
d.in_list_view = 1
|
||||
cnt += 1
|
||||
if cnt == 4: break
|
||||
|
|
@ -171,7 +171,8 @@ class DocType(Document):
|
|||
"""Change the timestamp of parent DocType if the current one is a child to clear caches."""
|
||||
if frappe.flags.in_import:
|
||||
return
|
||||
parent_list = frappe.db.get_all('DocField', 'parent', dict(fieldtype='Table', options=self.name))
|
||||
parent_list = frappe.db.get_all('DocField', 'parent',
|
||||
dict(fieldtype=['in', frappe.model.table_fields], options=self.name))
|
||||
for p in parent_list:
|
||||
frappe.db.sql('UPDATE `tabDocType` SET modified=%s WHERE `name`=%s', (now(), p.parent))
|
||||
|
||||
|
|
@ -511,11 +512,11 @@ def validate_fields(meta):
|
|||
validate_column_length(fieldname)
|
||||
|
||||
def check_illegal_mandatory(d):
|
||||
if (d.fieldtype in no_value_fields) and d.fieldtype!="Table" and d.reqd:
|
||||
if (d.fieldtype in no_value_fields) and d.fieldtype not in table_fields and d.reqd:
|
||||
frappe.throw(_("Field {0} of type {1} cannot be mandatory").format(d.label, d.fieldtype))
|
||||
|
||||
def check_link_table_options(d):
|
||||
if d.fieldtype in ("Link", "Table"):
|
||||
if d.fieldtype in ("Link",) + table_fields:
|
||||
if not d.options:
|
||||
frappe.throw(_("Options required for Link or Table type field {0} in row {1}").format(d.label, d.idx))
|
||||
if d.options=="[Select]" or d.options==d.parent:
|
||||
|
|
@ -692,6 +693,19 @@ def validate_fields(meta):
|
|||
re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", depends_on):
|
||||
frappe.throw(_("Invalid {0} condition").format(frappe.unscrub(field)), frappe.ValidationError)
|
||||
|
||||
def check_table_multiselect_option(docfield):
|
||||
'''check if the doctype provided in Option has atleast 1 Link field'''
|
||||
if not docfield.fieldtype == 'Table MultiSelect': return
|
||||
|
||||
doctype = docfield.options
|
||||
meta = frappe.get_meta(doctype)
|
||||
link_field = [df for df in meta.fields if df.fieldtype == 'Link']
|
||||
|
||||
if not link_field:
|
||||
frappe.throw(_('DocType <b>{0}</b> provided for the field <b>{1}</b> must have atleast one Link field')
|
||||
.format(doctype, docfield.fieldname), frappe.ValidationError)
|
||||
|
||||
|
||||
fields = meta.get("fields")
|
||||
fieldname_list = [d.fieldname for d in fields]
|
||||
|
||||
|
|
@ -702,7 +716,7 @@ def validate_fields(meta):
|
|||
|
||||
for d in fields:
|
||||
if not d.permlevel: d.permlevel = 0
|
||||
if d.fieldtype != "Table": d.allow_bulk_edit = 0
|
||||
if d.fieldtype not in table_fields: d.allow_bulk_edit = 0
|
||||
if d.fieldtype == "Barcode": d.ignore_xss_filter = 1
|
||||
if not d.fieldname:
|
||||
d.fieldname = d.fieldname.lower()
|
||||
|
|
@ -719,6 +733,7 @@ def validate_fields(meta):
|
|||
check_illegal_default(d)
|
||||
check_unique_and_text(d)
|
||||
check_illegal_depends_on_conditions(d)
|
||||
check_table_multiselect_option(d)
|
||||
|
||||
check_fold(fields)
|
||||
check_search_fields(meta, fields)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
|||
import frappe, json
|
||||
|
||||
from frappe.model.document import Document
|
||||
from frappe.model import no_value_fields
|
||||
from frappe.model import no_value_fields, table_fields
|
||||
|
||||
class Version(Document):
|
||||
def set_diff(self, old, new):
|
||||
|
|
@ -42,12 +42,12 @@ def get_diff(old, new, for_child=False):
|
|||
}'''
|
||||
out = frappe._dict(changed = [], added = [], removed = [], row_changed = [])
|
||||
for df in new.meta.fields:
|
||||
if df.fieldtype in no_value_fields and df.fieldtype != 'Table':
|
||||
if df.fieldtype in no_value_fields and df.fieldtype not in table_fields:
|
||||
continue
|
||||
|
||||
old_value, new_value = old.get(df.fieldname), new.get(df.fieldname)
|
||||
|
||||
if df.fieldtype=='Table':
|
||||
if df.fieldtype in table_fields:
|
||||
# make maps
|
||||
old_row_by_name, new_row_by_name = {}, {}
|
||||
for d in old_value:
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@
|
|||
"no_copy": 0,
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime\nSignature",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
|
|
@ -1302,7 +1302,7 @@
|
|||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-11-23 19:56:43.328280",
|
||||
"modified": "2018-12-19 18:34:46.031246",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Custom Field",
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ docfield_properties = {
|
|||
|
||||
allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'),
|
||||
('Text', 'Data'), ('Text', 'Text Editor', 'Code', 'Signature', 'HTML Editor'), ('Data', 'Select'),
|
||||
('Text', 'Small Text'), ('Text', 'Data', 'Barcode'), ('Code', 'Geolocation'))
|
||||
('Text', 'Small Text'), ('Text', 'Data', 'Barcode'), ('Code', 'Geolocation'), ('Table', 'Table MultiSelect'))
|
||||
|
||||
allowed_fieldtype_for_options_change = ('Read Only', 'HTML', 'Select', 'Data')
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -37,12 +37,16 @@ class PropertySetter(Document):
|
|||
and property = %(property)s""", self.get_valid_dict())
|
||||
|
||||
def get_property_list(self, dt):
|
||||
return frappe.db.sql("""select fieldname, label, fieldtype
|
||||
from tabDocField
|
||||
where parent=%s
|
||||
and fieldtype not in ('Section Break', 'Column Break', 'HTML', 'Read Only', 'Table', 'Fold')
|
||||
and coalesce(fieldname, '') != ''
|
||||
order by label asc""", dt, as_dict=1)
|
||||
return frappe.db.get_all('DocField',
|
||||
fields=['fieldname', 'label', 'fieldtype'],
|
||||
filters={
|
||||
'parent': dt,
|
||||
'fieldtype': ['not in', ('Section Break', 'Column Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields],
|
||||
'fieldname': ['!=', '']
|
||||
},
|
||||
order_by='label asc',
|
||||
as_dict=1
|
||||
)
|
||||
|
||||
def get_setup_data(self):
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -42,9 +42,10 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
|
|||
link_meta_bundle = frappe.desk.form.load.get_meta_bundle(dt)
|
||||
linkmeta = link_meta_bundle[0]
|
||||
if not linkmeta.get("issingle"):
|
||||
fields = [d.fieldname for d in linkmeta.get("fields", {"in_list_view":1,
|
||||
"fieldtype": ["not in", ["Image", "HTML", "Button", "Table"]]})] \
|
||||
+ ["name", "modified", "docstatus"]
|
||||
fields = [d.fieldname for d in linkmeta.get("fields", {
|
||||
"in_list_view": 1,
|
||||
"fieldtype": ["not in", ("Image", "HTML", "Button") + frappe.model.table_fields]
|
||||
})] + ["name", "modified", "docstatus"]
|
||||
|
||||
if link.get("add_fields"):
|
||||
fields += link["add_fields"]
|
||||
|
|
@ -116,7 +117,7 @@ def _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False)
|
|||
ret.update(get_linked_fields(doctype, without_ignore_user_permissions_enabled))
|
||||
ret.update(get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled))
|
||||
|
||||
filters=[['fieldtype','=','Table'], ['options', '=', doctype]]
|
||||
filters=[['fieldtype', 'in', frappe.model.table_fields], ['options', '=', doctype]]
|
||||
if without_ignore_user_permissions_enabled: filters.append(['ignore_user_permissions', '!=', 1])
|
||||
# find links of parents
|
||||
links = frappe.get_all("DocField", fields=["parent as dt"], filters=filters)
|
||||
|
|
@ -159,7 +160,7 @@ def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False):
|
|||
for doctype_name in links_dict:
|
||||
ret[doctype_name] = { "fieldname": links_dict.get(doctype_name) }
|
||||
table_doctypes = frappe.get_all("DocType", filters=[["istable", "=", "1"], ["name", "in", tuple(links_dict)]])
|
||||
child_filters = [['fieldtype','=', 'Table'], ['options', 'in', tuple(doctype.name for doctype in table_doctypes)]]
|
||||
child_filters = [['fieldtype','in', frappe.model.table_fields], ['options', 'in', tuple(doctype.name for doctype in table_doctypes)]]
|
||||
if without_ignore_user_permissions_enabled: child_filters.append(['ignore_user_permissions', '!=', 1])
|
||||
|
||||
# find out if linked in a child table
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ def getdoctype(doctype, with_parent=False, cached_timestamp=None):
|
|||
def get_meta_bundle(doctype):
|
||||
bundle = [frappe.desk.form.meta.get_meta(doctype)]
|
||||
for df in bundle[0].fields:
|
||||
if df.fieldtype=="Table":
|
||||
if df.fieldtype in frappe.model.table_fields:
|
||||
bundle.append(frappe.desk.form.meta.get_meta(df.options, not frappe.conf.developer_mode))
|
||||
return bundle
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ frappe.webhook = {
|
|||
frappe.model.with_doctype(doc.webhook_doctype, function() {
|
||||
var fields = $.map(frappe.get_doc("DocType", frm.doc.webhook_doctype).fields, function(d) {
|
||||
if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 ||
|
||||
d.fieldtype === 'Table') {
|
||||
frappe.model.table_fields.includes(d.fieldtype)) {
|
||||
return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname };
|
||||
}
|
||||
else if (d.fieldtype === 'Currency' || d.fieldtype === 'Float') {
|
||||
|
|
|
|||
|
|
@ -37,12 +37,13 @@ data_fieldtypes = (
|
|||
'Geolocation'
|
||||
)
|
||||
|
||||
no_value_fields = ('Section Break', 'Column Break', 'HTML', 'Table', 'Button', 'Image',
|
||||
no_value_fields = ('Section Break', 'Column Break', 'HTML', 'Table', 'Table MultiSelect', 'Button', 'Image',
|
||||
'Fold', 'Heading')
|
||||
display_fieldtypes = ('Section Break', 'Column Break', 'HTML', 'Button', 'Image', 'Fold', 'Heading')
|
||||
default_fields = ('doctype','name','owner','creation','modified','modified_by',
|
||||
'parent','parentfield','parenttype','idx','docstatus')
|
||||
optional_fields = ("_user_tags", "_comments", "_assign", "_liked_by", "_seen")
|
||||
table_fields = ('Table', 'Table MultiSelect')
|
||||
|
||||
def delete_fields(args_dict, delete=0):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from six import iteritems, string_types
|
|||
import frappe
|
||||
import datetime
|
||||
from frappe import _
|
||||
from frappe.model import default_fields
|
||||
from frappe.model import default_fields, table_fields
|
||||
from frappe.model.naming import set_new_name
|
||||
from frappe.model.utils.link_count import notify_link_count
|
||||
from frappe.modules import load_doctype_module
|
||||
|
|
@ -222,7 +222,7 @@ class BaseDocument(object):
|
|||
# unique empty field should be set to None
|
||||
d[fieldname] = None
|
||||
|
||||
if isinstance(d[fieldname], list) and df.fieldtype != 'Table':
|
||||
if isinstance(d[fieldname], list) and df.fieldtype not in table_fields:
|
||||
frappe.throw(_('Value for {0} cannot be a list').format(_(df.label)))
|
||||
|
||||
if convert_dates_to_str and isinstance(d[fieldname], (datetime.datetime, datetime.time, datetime.timedelta)):
|
||||
|
|
@ -398,7 +398,7 @@ class BaseDocument(object):
|
|||
def _get_missing_mandatory_fields(self):
|
||||
"""Get mandatory fields that do not have any values"""
|
||||
def get_msg(df):
|
||||
if df.fieldtype == "Table":
|
||||
if df.fieldtype in table_fields:
|
||||
return "{}: {}: {}".format(_("Error"), _("Data missing in table"), _(df.label))
|
||||
|
||||
elif self.parentfield:
|
||||
|
|
@ -573,7 +573,7 @@ class BaseDocument(object):
|
|||
db_value = db_values.get(key)
|
||||
|
||||
if df and not df.allow_on_submit and (self.get(key) or db_value):
|
||||
if df.fieldtype=="Table":
|
||||
if df.fieldtype in table_fields:
|
||||
# just check if the table size has changed
|
||||
# individual fields will be checked in the loop for children
|
||||
self_value = len(self.get(key))
|
||||
|
|
|
|||
|
|
@ -141,8 +141,14 @@ def delete_from_table(doctype, name, ignore_doctypes, doc):
|
|||
|
||||
else:
|
||||
def get_table_fields(field_doctype):
|
||||
return frappe.db.sql_list("""select options from `tab{}` where fieldtype='Table'
|
||||
and parent=%s""".format(field_doctype), doctype)
|
||||
return [r[0] for r in frappe.get_all(field_doctype,
|
||||
fields='options',
|
||||
filters={
|
||||
'fieldtype': ['in', frappe.model.table_fields],
|
||||
'parent': doctype
|
||||
},
|
||||
as_list=1
|
||||
)]
|
||||
|
||||
tables = get_table_fields("DocField")
|
||||
if not frappe.flags.in_install=="frappe":
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ def update_table(f, new):
|
|||
|
||||
def update_parent_field(f, new):
|
||||
"""update 'parentfield' in tables"""
|
||||
if f['fieldtype']=='Table':
|
||||
if f['fieldtype'] in frappe.model.table_fields:
|
||||
frappe.db.begin()
|
||||
frappe.db.sql("""update `tab%s` set parentfield=%s where parentfield=%s""" \
|
||||
% (f['options'], '%s', '%s'), (new, f['fieldname']))
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from frappe.model.naming import set_new_name
|
|||
from six import iteritems, string_types
|
||||
from werkzeug.exceptions import NotFound, Forbidden
|
||||
import hashlib, json
|
||||
from frappe.model import optional_fields
|
||||
from frappe.model import optional_fields, table_fields
|
||||
from frappe.model.workflow import validate_workflow
|
||||
from frappe.utils.global_search import update_global_search
|
||||
from frappe.integrations.doctype.webhook import run_webhooks
|
||||
|
|
@ -489,7 +489,7 @@ class Document(BaseDocument):
|
|||
value = self.get(field.fieldname)
|
||||
original_value = self._doc_before_save.get(field.fieldname)
|
||||
|
||||
if field.fieldtype=='Table':
|
||||
if field.fieldtype in table_fields:
|
||||
fail = not self.is_child_table_same(field.fieldname)
|
||||
elif field.fieldtype in ('Date', 'Datetime', 'Time'):
|
||||
fail = str(value) != str(original_value)
|
||||
|
|
@ -756,7 +756,7 @@ class Document(BaseDocument):
|
|||
def get_all_children(self, parenttype=None):
|
||||
"""Returns all children documents from **Table** type field in a list."""
|
||||
ret = []
|
||||
for df in self.meta.get("fields", {"fieldtype": "Table"}):
|
||||
for df in self.meta.get("fields", {"fieldtype": ['in', table_fields]}):
|
||||
if parenttype:
|
||||
if df.options==parenttype:
|
||||
return self.get(df.fieldname)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from __future__ import unicode_literals
|
|||
import frappe, json
|
||||
from frappe import _
|
||||
from frappe.utils import cstr
|
||||
from frappe.model import default_fields
|
||||
from frappe.model import default_fields, table_fields
|
||||
from six import string_types
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -129,8 +129,8 @@ def map_doc(source_doc, target_doc, table_map, source_parent=None):
|
|||
table_map["postprocess"](source_doc, target_doc, source_parent)
|
||||
|
||||
def map_fields(source_doc, target_doc, table_map, source_parent):
|
||||
no_copy_fields = set([d.fieldname for d in source_doc.meta.get("fields") if (d.no_copy==1 or d.fieldtype=="Table")]
|
||||
+ [d.fieldname for d in target_doc.meta.get("fields") if (d.no_copy==1 or d.fieldtype=="Table")]
|
||||
no_copy_fields = set([d.fieldname for d in source_doc.meta.get("fields") if (d.no_copy==1 or d.fieldtype in table_fields)]
|
||||
+ [d.fieldname for d in target_doc.meta.get("fields") if (d.no_copy==1 or d.fieldtype in table_fields)]
|
||||
+ list(default_fields)
|
||||
+ list(table_map.get("field_no_map", [])))
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from datetime import datetime
|
|||
from six.moves import range
|
||||
import frappe, json, os
|
||||
from frappe.utils import cstr, cint
|
||||
from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes
|
||||
from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.base_document import BaseDocument
|
||||
from frappe.modules import load_doctype_module
|
||||
|
|
@ -150,7 +150,7 @@ class Meta(Document):
|
|||
def get_table_fields(self):
|
||||
if not hasattr(self, "_table_fields"):
|
||||
if self.name!="DocType":
|
||||
self._table_fields = self.get('fields', {"fieldtype":"Table"})
|
||||
self._table_fields = self.get('fields', {"fieldtype": ['in', table_fields]})
|
||||
else:
|
||||
self._table_fields = doctype_table_fields
|
||||
|
||||
|
|
@ -451,7 +451,7 @@ def is_single(doctype):
|
|||
raise Exception('Cannot determine whether %s is single' % doctype)
|
||||
|
||||
def get_parent_dt(dt):
|
||||
parent_dt = frappe.db.get_all('DocField', 'parent', dict(fieldtype='Table', options=dt), limit=1)
|
||||
parent_dt = frappe.db.get_all('DocField', 'parent', dict(fieldtype=['in', frappe.model.table_fields], options=dt), limit=1)
|
||||
return parent_dt and parent_dt[0].parent or ''
|
||||
|
||||
def set_fieldname(field_id, fieldname):
|
||||
|
|
|
|||
|
|
@ -173,9 +173,11 @@ def validate_rename(doctype, new, meta, merge, force, ignore_permissions):
|
|||
return new
|
||||
|
||||
def rename_doctype(doctype, old, new, force=False):
|
||||
# change options for fieldtype Table
|
||||
update_options_for_fieldtype("Table", old, new)
|
||||
update_options_for_fieldtype("Link", old, new)
|
||||
# change options for fieldtype Table, Table MultiSelect and Link
|
||||
fields_with_options = ("Link",) + frappe.model.table_fields
|
||||
|
||||
for fieldtype in fields_with_options:
|
||||
update_options_for_fieldtype(fieldtype, old, new)
|
||||
|
||||
# change options where select options are hardcoded i.e. listed
|
||||
select_fields = get_select_fields(old, new)
|
||||
|
|
@ -352,13 +354,21 @@ def update_select_field_values(old, new):
|
|||
.format(frappe.db.escape('%' + '\n' + old + '%'), frappe.db.escape('%' + old + '\n' + '%')), (old, new, new))
|
||||
|
||||
def update_parenttype_values(old, new):
|
||||
child_doctypes = frappe.db.sql("""\
|
||||
select options, fieldname from `tabDocField`
|
||||
where parent=%s and fieldtype='Table'""", (new,), as_dict=1)
|
||||
child_doctypes = frappe.db.get_all('DocField',
|
||||
fields=['options', 'fieldname'],
|
||||
filters={
|
||||
'parent': new,
|
||||
'fieldtype': ['in', frappe.model.table_fields]
|
||||
}
|
||||
)
|
||||
|
||||
custom_child_doctypes = frappe.db.sql("""\
|
||||
select options, fieldname from `tabCustom Field`
|
||||
where dt=%s and fieldtype='Table'""", (new,), as_dict=1)
|
||||
custom_child_doctypes = frappe.db.get_all('Custom Field',
|
||||
fields=['options', 'fieldname'],
|
||||
filters={
|
||||
'dt': new,
|
||||
'fieldtype': ['in', frappe.model.table_fields]
|
||||
}
|
||||
)
|
||||
|
||||
child_doctypes += custom_child_doctypes
|
||||
fields = [d['fieldname'] for d in child_doctypes]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from __future__ import unicode_literals, print_function
|
|||
|
||||
import frappe
|
||||
import json
|
||||
from frappe.model import no_value_fields
|
||||
from frappe.model import no_value_fields, table_fields
|
||||
from frappe.utils.password import rename_password_field
|
||||
from frappe.model.utils.user_settings import update_user_settings_data, sync_user_settings
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ def rename_field(doctype, old_fieldname, new_fieldname):
|
|||
print("rename_field: " + (new_fieldname) + " not found in " + doctype)
|
||||
return
|
||||
|
||||
if new_field.fieldtype == "Table":
|
||||
if new_field.fieldtype in table_fields:
|
||||
# change parentfield of table mentioned in options
|
||||
frappe.db.sql("""update `tab%s` set parentfield=%s
|
||||
where parentfield=%s""" % (new_field.options.split("\n")[0], "%s", "%s"),
|
||||
|
|
|
|||
|
|
@ -85,7 +85,8 @@
|
|||
"public/js/frappe/form/controls/barcode.js",
|
||||
"public/js/frappe/form/controls/geolocation.js",
|
||||
"public/js/frappe/form/controls/multiselect.js",
|
||||
"public/js/frappe/form/controls/multicheck.js"
|
||||
"public/js/frappe/form/controls/multicheck.js",
|
||||
"public/js/frappe/form/controls/table_multiselect.js"
|
||||
],
|
||||
"js/dialog.min.js": [
|
||||
"public/js/frappe/dom.js",
|
||||
|
|
|
|||
130
frappe/public/js/frappe/form/controls/table_multiselect.js
Normal file
130
frappe/public/js/frappe/form/controls/table_multiselect.js
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
frappe.ui.form.ControlTableMultiSelect = frappe.ui.form.ControlLink.extend({
|
||||
make_input() {
|
||||
this._super();
|
||||
|
||||
this.$input_area.addClass('form-control table-multiselect');
|
||||
this.$input.removeClass('form-control');
|
||||
|
||||
this.$input.on("awesomplete-selectcomplete", () => {
|
||||
this.$input.val('');
|
||||
});
|
||||
|
||||
// used as an internal model to store values
|
||||
this.rows = [];
|
||||
|
||||
this.$input_area.on('click', '.btn-remove', (e) => {
|
||||
const $target = $(e.currentTarget);
|
||||
const $value = $target.closest('.tb-selected-value');
|
||||
|
||||
const value = decodeURIComponent($value.data().value);
|
||||
const link_field = this.get_link_field();
|
||||
this.rows = this.rows.filter(row => row[link_field.fieldname] !== value);
|
||||
|
||||
this.parse_validate_and_set_in_model('');
|
||||
});
|
||||
this.$input_area.on('click', '.btn-link-to-form', (e) => {
|
||||
const $target = $(e.currentTarget);
|
||||
const $value = $target.closest('.tb-selected-value');
|
||||
|
||||
const value = decodeURIComponent($value.data().value);
|
||||
const link_field = this.get_link_field();
|
||||
frappe.set_route('Form', link_field.options, value);
|
||||
});
|
||||
},
|
||||
setup_buttons() {
|
||||
this.$input_area.find('.link-btn').remove();
|
||||
},
|
||||
parse(value) {
|
||||
const link_field = this.get_link_field();
|
||||
|
||||
if (value) {
|
||||
if (this.frm) {
|
||||
const new_row = frappe.model.add_child(this.frm.doc, this.df.options, this.df.fieldname);
|
||||
new_row[link_field.fieldname] = value;
|
||||
} else {
|
||||
this.rows.push({
|
||||
[link_field.fieldname]: value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return this.rows;
|
||||
},
|
||||
validate(value) {
|
||||
const rows = (value || []).slice();
|
||||
|
||||
// validate the value just entered
|
||||
if (this.df.ignore_link_validation) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const link_field = this.get_link_field();
|
||||
if (rows.length === 0) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const all_rows_except_last = rows.slice(0, rows.length - 1);
|
||||
const last_row = rows[rows.length - 1];
|
||||
|
||||
// validate the last value entered
|
||||
const link_value = last_row[link_field.fieldname];
|
||||
|
||||
// falsy value
|
||||
if (!link_value) {
|
||||
return all_rows_except_last;
|
||||
}
|
||||
|
||||
// duplicate value
|
||||
if (all_rows_except_last.map(row => row[link_field.fieldname]).includes(link_value)) {
|
||||
return all_rows_except_last;
|
||||
}
|
||||
|
||||
const validate_promise = this.validate_link_and_fetch(this.df, this.get_options(),
|
||||
this.docname, link_value);
|
||||
|
||||
return validate_promise.then(validated_value => {
|
||||
if (validated_value === link_value) {
|
||||
return rows;
|
||||
} else {
|
||||
rows.pop();
|
||||
return rows;
|
||||
}
|
||||
});
|
||||
},
|
||||
set_formatted_input(value) {
|
||||
this.rows = value || [];
|
||||
const link_field = this.get_link_field();
|
||||
const values = this.rows.map(row => row[link_field.fieldname]);
|
||||
this.set_pill_html(values);
|
||||
},
|
||||
set_pill_html(values) {
|
||||
const html = values
|
||||
.map(value => this.get_pill_html(value))
|
||||
.join('');
|
||||
|
||||
this.$input_area.find('.tb-selected-value').remove();
|
||||
this.$input_area.prepend(html);
|
||||
},
|
||||
get_pill_html(value) {
|
||||
const encoded_value = encodeURIComponent(value);
|
||||
return `<div class="btn-group tb-selected-value" data-value="${encoded_value}">
|
||||
<button class="btn btn-default btn-xs btn-link-to-form">${__(value)}</button>
|
||||
<button class="btn btn-default btn-xs btn-remove">
|
||||
<i class="fa fa-remove text-muted"></i>
|
||||
</button>
|
||||
</div>`;
|
||||
},
|
||||
get_options() {
|
||||
return (this.get_link_field() || {}).options;
|
||||
},
|
||||
get_link_field() {
|
||||
if (!this._link_field) {
|
||||
const meta = frappe.get_meta(this.df.options);
|
||||
this._link_field = meta.fields.find(df => df.fieldtype === 'Link');
|
||||
if (!this._link_field) {
|
||||
throw new Error('Table MultiSelect requires a Table with atleast one Link field');
|
||||
}
|
||||
}
|
||||
return this._link_field;
|
||||
},
|
||||
});
|
||||
|
|
@ -238,6 +238,16 @@ frappe.form.formatters = {
|
|||
value = flt(flt(value) / 1024, 1) + "K";
|
||||
}
|
||||
return value;
|
||||
},
|
||||
TableMultiSelect: function(rows, df, options) {
|
||||
rows = rows || [];
|
||||
const meta = frappe.get_meta(df.options);
|
||||
const link_field = meta.fields.find(df => df.fieldtype === 'Link');
|
||||
const formatted_values = rows.map(row => {
|
||||
const value = row[link_field.fieldname];
|
||||
return frappe.format(value, link_field, options, row);
|
||||
});
|
||||
return formatted_values.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ frappe.ui.form.ScriptManager = Class.extend({
|
|||
// setup add fetch
|
||||
$.each(this.frm.fields, function(i, field) {
|
||||
setup_add_fetch(field.df);
|
||||
if(field.df.fieldtype==="Table") {
|
||||
if(frappe.model.table_fields.includes(field.df.fieldtype)) {
|
||||
$.each(frappe.meta.get_docfields(field.df.options, me.frm.docname), function(i, df) {
|
||||
setup_add_fetch(df);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ $.extend(frappe.model, {
|
|||
&& !(df && (!from_amend && cint(df.no_copy) == 1))) {
|
||||
|
||||
var value = doc[key] || [];
|
||||
if (df.fieldtype === "Table") {
|
||||
if (frappe.model.table_fields.includes(df.fieldtype)) {
|
||||
for (var i = 0, j = value.length; i < j; i++) {
|
||||
var d = value[i];
|
||||
frappe.model.copy_doc(d, from_amend, newdoc, df.fieldname);
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ $.extend(frappe.meta, {
|
|||
|
||||
get_table_fields: function(dt) {
|
||||
return $.map(frappe.meta.docfield_list[dt], function(d) {
|
||||
return d.fieldtype==='Table' ? d : null});
|
||||
return frappe.model.table_fields.includes(d.fieldtype) ? d : null});
|
||||
},
|
||||
|
||||
get_doctype_for_field: function(doctype, key) {
|
||||
|
|
@ -168,8 +168,8 @@ $.extend(frappe.meta, {
|
|||
},
|
||||
|
||||
get_parentfield: function(parent_dt, child_dt) {
|
||||
var df = (frappe.get_doc("DocType", parent_dt).fields || []).filter(function(d)
|
||||
{ return d.fieldtype==="Table" && d.options===child_dt })
|
||||
var df = (frappe.get_doc("DocType", parent_dt).fields || [])
|
||||
.filter(df => frappe.model.table_fields.includes(df.fieldtype) && df.options===child_dt)
|
||||
if(!df.length)
|
||||
throw "parentfield not found for " + parent_dt + ", " + child_dt;
|
||||
return df[0].fieldname;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
frappe.provide('frappe.model');
|
||||
|
||||
$.extend(frappe.model, {
|
||||
no_value_type: ['Section Break', 'Column Break', 'HTML', 'Table',
|
||||
no_value_type: ['Section Break', 'Column Break', 'HTML', 'Table', 'Table MultiSelect',
|
||||
'Button', 'Image', 'Fold', 'Heading'],
|
||||
|
||||
layout_fields: ['Section Break', 'Column Break', 'Fold'],
|
||||
|
|
@ -33,6 +33,8 @@ $.extend(frappe.model, {
|
|||
{fieldname:'parent', fieldtype:'Data', label:__('Parent')},
|
||||
],
|
||||
|
||||
table_fields: ['Table', 'Table MultiSelect'],
|
||||
|
||||
new_names: {},
|
||||
events: {},
|
||||
user_settings: {},
|
||||
|
|
@ -96,10 +98,12 @@ $.extend(frappe.model, {
|
|||
if(locals.DocType[doctype]) {
|
||||
callback && callback();
|
||||
} else {
|
||||
var cached_timestamp = null;
|
||||
let cached_timestamp = null;
|
||||
let cached_doc = null;
|
||||
|
||||
if(localStorage["_doctype:" + doctype]) {
|
||||
let cached_docs = JSON.parse(localStorage["_doctype:" + doctype]);
|
||||
let cached_doc = cached_docs.filter(doc => doc.name === doctype)[0];
|
||||
cached_doc = cached_docs.filter(doc => doc.name === doctype)[0];
|
||||
if(cached_doc) {
|
||||
cached_timestamp = cached_doc.modified;
|
||||
}
|
||||
|
|
@ -304,7 +308,7 @@ $.extend(frappe.model, {
|
|||
var val = locals[dt] && locals[dt][dn] && locals[dt][dn][fn];
|
||||
var df = frappe.meta.get_docfield(dt, fn, dn);
|
||||
|
||||
if(df.fieldtype=='Table') {
|
||||
if(frappe.model.table_fields.includes(df.fieldtype)) {
|
||||
var ret = false;
|
||||
$.each(locals[df.options] || {}, function(k,d) {
|
||||
if(d.parent==dn && d.parenttype==dt && d.parentfield==df.fieldname) {
|
||||
|
|
|
|||
|
|
@ -106,10 +106,10 @@ $.extend(frappe.model, {
|
|||
if (source[key] == undefined) delete target[key];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
for (let fieldname in doc) {
|
||||
let df = frappe.meta.get_field(doc.doctype, fieldname);
|
||||
if (df && df.fieldtype === 'Table') {
|
||||
if (df && frappe.model.table_fields.includes(df.fieldtype)) {
|
||||
// table
|
||||
if (!(doc[fieldname] instanceof Array)) {
|
||||
doc[fieldname] = [];
|
||||
|
|
@ -118,7 +118,7 @@ $.extend(frappe.model, {
|
|||
if (!(local_doc[fieldname] instanceof Array)) {
|
||||
local_doc[fieldname] = [];
|
||||
}
|
||||
|
||||
|
||||
// child table, override each row and append new rows if required
|
||||
for (let i=0; i < doc[fieldname].length; i++ ) {
|
||||
let d = doc[fieldname][i];
|
||||
|
|
@ -144,7 +144,7 @@ $.extend(frappe.model, {
|
|||
// row exists, just copy the values
|
||||
Object.assign(local_d, d);
|
||||
clear_keys(d, local_d);
|
||||
|
||||
|
||||
} else {
|
||||
local_doc[fieldname].push(d);
|
||||
if (!d.parent) d.parent = doc.name;
|
||||
|
|
@ -170,7 +170,7 @@ $.extend(frappe.model, {
|
|||
local_doc[fieldname] = doc[fieldname];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// clear keys on parent
|
||||
clear_keys(doc, local_doc);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ frappe.ui.FieldSelect = Class.extend({
|
|||
if(me.doctype && df.parent==me.doctype) {
|
||||
label = __(df.label);
|
||||
table = me.doctype;
|
||||
if(df.fieldtype=='Table') me.table_fields.push(df);
|
||||
if(frappe.model.table_fields.includes(df.fieldtype)) me.table_fields.push(df);
|
||||
} else {
|
||||
label = __(df.label) + ' (' + __(df.parent) + ')';
|
||||
table = df.parent;
|
||||
|
|
|
|||
|
|
@ -658,7 +658,7 @@ frappe.ui.FieldSelect = Class.extend({
|
|||
if(me.doctype && df.parent==me.doctype) {
|
||||
var label = __(df.label);
|
||||
var table = me.doctype;
|
||||
if(df.fieldtype=='Table') me.table_fields.push(df);
|
||||
if(frappe.model.table_fields.includes(df.fieldtype)) me.table_fields.push(df);
|
||||
} else {
|
||||
var label = __(df.label) + ' (' + __(df.parent) + ')';
|
||||
var table = df.parent;
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
|
|||
// quick entry
|
||||
var mandatory = meta.fields.filter((df) => df.reqd && !doc[df.fieldname]);
|
||||
|
||||
if (mandatory.some(df => df.fieldtype === 'Table') || mandatory.length > 1) {
|
||||
if (mandatory.some(df => frappe.model.table_fields.includes(df.fieldtype)) || mandatory.length > 1) {
|
||||
quick_entry = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ _f.Frm.prototype.set_value = function(field, value, if_missing) {
|
|||
var fieldobj = me.fields_dict[f];
|
||||
if(fieldobj) {
|
||||
if(!if_missing || !frappe.model.has_value(me.doctype, me.doc.name, f)) {
|
||||
if(fieldobj.df.fieldtype==="Table" && $.isArray(v)) {
|
||||
if(frappe.model.table_fields.includes(fieldobj.df.fieldtype) && $.isArray(v)) {
|
||||
|
||||
frappe.model.clear_table(me.doc, fieldobj.df.fieldname);
|
||||
|
||||
|
|
|
|||
|
|
@ -213,7 +213,9 @@ _f.Frm.prototype.watch_model_updates = function() {
|
|||
});
|
||||
|
||||
// on table fields
|
||||
var table_fields = frappe.get_children("DocType", me.doctype, "fields", {fieldtype:"Table"});
|
||||
var table_fields = frappe.get_children("DocType", me.doctype, "fields", {
|
||||
fieldtype: ["in", frappe.model.table_fields]
|
||||
});
|
||||
|
||||
// using $.each to preserve df via closure
|
||||
$.each(table_fields, function(i, df) {
|
||||
|
|
|
|||
|
|
@ -54,3 +54,26 @@
|
|||
.markdown-toggle, .html-toggle {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* table multiselect */
|
||||
.table-multiselect {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.table-multiselect.form-control input {
|
||||
outline: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-size: @text-medium;
|
||||
}
|
||||
|
||||
.tb-selected-value {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,4 +81,10 @@ def format_value(value, df=None, doc=None, currency=None, translated=False):
|
|||
elif df.get("fieldtype") == "Markdown Editor":
|
||||
return frappe.utils.markdown(value)
|
||||
|
||||
elif df.get("fieldtype") == "Table MultiSelect":
|
||||
meta = frappe.get_meta(df.options)
|
||||
link_field = [df for df in meta.fields if df.fieldtype == 'Link'][0]
|
||||
values = [v.get(link_field.fieldname, 'asdf') for v in value]
|
||||
return ', '.join(values)
|
||||
|
||||
return value
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ def rebuild_for_doctype(doctype):
|
|||
meta = frappe.get_meta(doctype)
|
||||
if cint(meta.istable) == 1:
|
||||
parent_doctypes = frappe.get_all("DocField", fields="parent", filters={
|
||||
"fieldtype": "Table",
|
||||
"fieldtype": ["in", frappe.model.table_fields],
|
||||
"options": doctype
|
||||
})
|
||||
for p in parent_doctypes:
|
||||
|
|
@ -229,7 +229,7 @@ def update_global_search(doc):
|
|||
|
||||
content = []
|
||||
for field in doc.meta.get_global_search_fields():
|
||||
if doc.get(field.fieldname) and field.fieldtype != "Table":
|
||||
if doc.get(field.fieldname) and field.fieldtype not in frappe.model.table_fields:
|
||||
content.append(get_formatted_value(doc.get(field.fieldname), field))
|
||||
|
||||
tags = (doc.get('_user_tags') or '').strip()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue