Merge branch 'develop' into offline-erpnext
This commit is contained in:
commit
a25f5d207a
49 changed files with 596 additions and 198 deletions
|
|
@ -107,5 +107,6 @@ install:
|
|||
- bench build --app frappe
|
||||
|
||||
after_script:
|
||||
- pip install coverage==4.5.4
|
||||
- pip install python-coveralls
|
||||
- coveralls -b apps/frappe -d ../../sites/.coverage
|
||||
|
|
|
|||
|
|
@ -19,11 +19,13 @@ context('Report View', () => {
|
|||
cy.server();
|
||||
cy.route('POST', 'api/method/frappe.client.set_value').as('value-update');
|
||||
cy.visit(`/desk#List/${doctype_name}/Report`);
|
||||
let cell = cy.get('.dt-row-0 > .dt-cell--col-3');
|
||||
// check status column added from docstatus
|
||||
cy.get('.dt-row-0 > .dt-cell--col-3').should('contain', 'Submitted');
|
||||
let cell = cy.get('.dt-row-0 > .dt-cell--col-4');
|
||||
// select the cell
|
||||
cell.dblclick();
|
||||
cell.find('input[data-fieldname="enabled"]').check({force: true});
|
||||
cy.get('.dt-row-0 > .dt-cell--col-4').click();
|
||||
cy.get('.dt-row-0 > .dt-cell--col-5').click();
|
||||
cy.wait('@value-update');
|
||||
cy.get('@doc').then(doc => {
|
||||
cy.call('frappe.client.get_value', {
|
||||
|
|
|
|||
|
|
@ -1537,7 +1537,7 @@ def logger(module=None, with_more_info=True):
|
|||
from frappe.utils.logger import get_logger
|
||||
return get_logger(module or 'default', with_more_info=with_more_info)
|
||||
|
||||
def log_error(message=None, title=None):
|
||||
def log_error(message=None, title=_("Error")):
|
||||
'''Log error to Error Log'''
|
||||
|
||||
# AI ALERT:
|
||||
|
|
@ -1546,9 +1546,8 @@ def log_error(message=None, title=None):
|
|||
# this hack tries to be smart about whats a title (single line ;-)) and fixes it
|
||||
|
||||
if message:
|
||||
if '\n' not in message:
|
||||
title = message
|
||||
error = get_traceback()
|
||||
if '\n' in title:
|
||||
error, title = title, message
|
||||
else:
|
||||
error = message
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ def clear_doctype_map(doctype, name):
|
|||
cache_key = frappe.scrub(doctype) + '_map'
|
||||
frappe.cache().hdel(cache_key, name)
|
||||
|
||||
def build_table_count_cache():
|
||||
def build_table_count_cache(*args, **kwargs):
|
||||
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
|
||||
return
|
||||
_cache = frappe.cache()
|
||||
|
|
@ -137,7 +137,7 @@ def build_table_count_cache():
|
|||
|
||||
return counts
|
||||
|
||||
def build_domain_restriced_doctype_cache():
|
||||
def build_domain_restriced_doctype_cache(*args, **kwargs):
|
||||
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
|
||||
return
|
||||
_cache = frappe.cache()
|
||||
|
|
@ -148,7 +148,7 @@ def build_domain_restriced_doctype_cache():
|
|||
|
||||
return doctypes
|
||||
|
||||
def build_domain_restriced_page_cache():
|
||||
def build_domain_restriced_page_cache(*args, **kwargs):
|
||||
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
|
||||
return
|
||||
_cache = frappe.cache()
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class TestDocType(unittest.TestCase):
|
|||
}],
|
||||
"permissions": [{
|
||||
"role": "System Manager",
|
||||
"read": 1
|
||||
"read": 1,
|
||||
}],
|
||||
"name": name
|
||||
})
|
||||
|
|
@ -295,3 +295,58 @@ class TestDocType(unittest.TestCase):
|
|||
field_1.search_index = 1
|
||||
|
||||
self.assertRaises(CannotIndexedError, doc.insert)
|
||||
|
||||
def test_cancel_link_doctype(self):
|
||||
import json
|
||||
from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs
|
||||
|
||||
#create doctype
|
||||
link_doc = self.new_doctype('Test Linked Doctype')
|
||||
link_doc.is_submittable = 1
|
||||
for data in link_doc.get('permissions'):
|
||||
data.submit = 1
|
||||
data.cancel = 1
|
||||
link_doc.insert()
|
||||
|
||||
doc = self.new_doctype('Test Doctype')
|
||||
doc.is_submittable = 1
|
||||
field_2 = doc.append('fields', {})
|
||||
field_2.label = 'Test Linked Doctype'
|
||||
field_2.fieldname = 'test_linked_doctype'
|
||||
field_2.fieldtype = 'Link'
|
||||
field_2.options = 'Test Linked Doctype'
|
||||
for data in link_doc.get('permissions'):
|
||||
data.submit = 1
|
||||
data.cancel = 1
|
||||
doc.insert()
|
||||
|
||||
# create doctype data
|
||||
data_link_doc = frappe.new_doc('Test Linked Doctype')
|
||||
data_link_doc.some_fieldname = 'Data1'
|
||||
data_link_doc.insert()
|
||||
data_link_doc.save()
|
||||
data_link_doc.submit()
|
||||
|
||||
data_doc = frappe.new_doc('Test Doctype')
|
||||
data_doc.some_fieldname = 'Data1'
|
||||
data_doc.test_linked_doctype = data_link_doc.name
|
||||
data_doc.insert()
|
||||
data_doc.save()
|
||||
data_doc.submit()
|
||||
|
||||
docs = get_submitted_linked_docs(link_doc.name, data_link_doc.name)
|
||||
dump_docs = json.dumps(docs.get('docs'))
|
||||
cancel_all_linked_docs(dump_docs)
|
||||
data_link_doc.cancel()
|
||||
data_doc.load_from_db()
|
||||
self.assertEqual(data_link_doc.docstatus, 2)
|
||||
self.assertEqual(data_doc.docstatus, 2)
|
||||
|
||||
# delete doctype record
|
||||
data_doc.delete()
|
||||
data_link_doc.delete()
|
||||
|
||||
# delete doctype
|
||||
link_doc.delete()
|
||||
doc.delete()
|
||||
frappe.db.commit()
|
||||
|
|
@ -126,13 +126,15 @@ class Report(Document):
|
|||
safe_exec(self.report_script, None, loc)
|
||||
return loc['data']
|
||||
|
||||
def get_data(self, filters=None, limit=None, user=None, as_dict=False):
|
||||
def get_data(self, filters=None, limit=None, user=None, as_dict=False, ignore_prepared_report=False):
|
||||
columns = []
|
||||
out = []
|
||||
|
||||
if self.report_type in ('Query Report', 'Script Report', 'Custom Report'):
|
||||
# query and script reports
|
||||
data = frappe.desk.query_report.run(self.name, filters=filters, user=user)
|
||||
data = frappe.desk.query_report.run(self.name,
|
||||
filters=filters, user=user, ignore_prepared_report=ignore_prepared_report)
|
||||
|
||||
for d in data.get('columns'):
|
||||
if isinstance(d, dict):
|
||||
col = frappe._dict(d)
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ frappe.ui.form.on('User', {
|
|||
email: frm.doc.email
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message == undefined) {
|
||||
if (!Array.isArray(r.message)) {
|
||||
frappe.route_options = {
|
||||
"email_id": frm.doc.email,
|
||||
"awaiting_password": 1,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<tbody>
|
||||
{% for j in jobs %}
|
||||
<tr>
|
||||
<td><span class="indicator {{ j.color }}" title="{{ j.get_status() }}">{{ j.queue.split(".").slice(-1)[0] }}</span></td>
|
||||
<td><span class="indicator {{ j.color }}" title="{{ j.status }}">{{ j.queue.split(".").slice(-1)[0] }}</span></td>
|
||||
<td style="overflow: auto;">
|
||||
<div>
|
||||
{{ frappe.utils.encode_tags(j.job_name) }}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,14 @@ frappe.ui.form.on("Customize Form", {
|
|||
};
|
||||
});
|
||||
|
||||
frm.set_query("default_print_format", function() {
|
||||
return {
|
||||
filters: {
|
||||
'print_format_type': ['!=', 'JS']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(frm.wrapper).on("grid-row-render", function(e, grid_row) {
|
||||
if(grid_row.doc && grid_row.doc.fieldtype=="Section Break") {
|
||||
$(grid_row.row).css({"font-weight": "bold"});
|
||||
|
|
|
|||
|
|
@ -114,9 +114,8 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
} else {
|
||||
// standard filters
|
||||
if (frm.doc.document_type) {
|
||||
// allow all link and select fields as filters
|
||||
frm.chart_filters = [];
|
||||
frappe.model.with_doctype(frm.doc.document_type, () => {
|
||||
frm.chart_filters = [];
|
||||
frappe.get_meta(frm.doc.document_type).fields.map(df => {
|
||||
if (['Link', 'Select'].includes(df.fieldtype)) {
|
||||
let _df = copy_dict(df);
|
||||
|
|
@ -131,8 +130,8 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
|
||||
frm.chart_filters.push(_df);
|
||||
}
|
||||
frm.trigger('render_filters_table');
|
||||
});
|
||||
frm.trigger('render_filters_table');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -158,7 +157,7 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
|
||||
let filters = JSON.parse(frm.doc.filters_json || '{}');
|
||||
var filters_set = false;
|
||||
fields.map( f => {
|
||||
fields.map(f => {
|
||||
if (filters[f.fieldname]) {
|
||||
const filter_row = $(`<tr><td>${f.label}</td><td>${filters[f.fieldname] || ""}</td></tr>`);
|
||||
table.find('tbody').append(filter_row);
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ class Event(Document):
|
|||
communication.subject = self.subject
|
||||
communication.content = self.description if self.description else self.subject
|
||||
communication.communication_date = self.starts_on
|
||||
communication.sender = self.owner
|
||||
communication.sender_full_name = frappe.utils.get_fullname(self.owner)
|
||||
communication.reference_doctype = self.doctype
|
||||
communication.reference_name = self.name
|
||||
communication.communication_medium = communication_mapping.get(self.event_category) if self.event_category else ""
|
||||
|
|
|
|||
|
|
@ -1,14 +1,119 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe, json
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from six import string_types
|
||||
import frappe
|
||||
import frappe.desk.form.load
|
||||
import frappe.desk.form.meta
|
||||
from frappe import _
|
||||
from frappe.model.meta import is_single
|
||||
from frappe.modules import load_doctype_module
|
||||
import frappe.desk.form.meta
|
||||
import frappe.desk.form.load
|
||||
from six import string_types
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_submitted_linked_docs(doctype, name, docs=None):
|
||||
"""
|
||||
Get all nested submitted linked doctype linkinfo
|
||||
|
||||
Arguments:
|
||||
doctype (str) - The doctype for which get all linked doctypes
|
||||
name (str) - The docname for which get all linked doctypes
|
||||
|
||||
Keyword Arguments:
|
||||
docs (list of dict) - (Optional) Get list of dictionary for linked doctype.
|
||||
|
||||
Returns:
|
||||
dict - Return list of documents and link count
|
||||
"""
|
||||
|
||||
if not docs:
|
||||
docs = []
|
||||
|
||||
linkinfo = get_linked_doctypes(doctype)
|
||||
linked_docs = get_linked_docs(doctype, name, linkinfo)
|
||||
|
||||
link_count = 0
|
||||
for link_doctype, link_names in linked_docs.items():
|
||||
for link in link_names:
|
||||
docinfo = link.update({"doctype": link_doctype})
|
||||
validated_doc = validate_linked_doc(docinfo)
|
||||
|
||||
if not validated_doc:
|
||||
continue
|
||||
|
||||
link_count += 1
|
||||
if link.name in [doc.get("name") for doc in docs]:
|
||||
continue
|
||||
|
||||
links = get_submitted_linked_docs(link_doctype, link.name, docs)
|
||||
docs.append({
|
||||
"doctype": link_doctype,
|
||||
"name": link.name,
|
||||
"docstatus": link.docstatus,
|
||||
"link_count": links.get("count")
|
||||
})
|
||||
|
||||
# sort linked documents by ascending number of links
|
||||
docs.sort(key=lambda doc: doc.get("link_count"))
|
||||
return {
|
||||
"docs": docs,
|
||||
"count": link_count
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_all_linked_docs(docs):
|
||||
"""
|
||||
Cancel all linked doctype
|
||||
|
||||
Arguments:
|
||||
docs (str) - It contains all list of dictionaries of a linked documents.
|
||||
"""
|
||||
|
||||
docs = json.loads(docs)
|
||||
for i, doc in enumerate(docs, 1):
|
||||
if validate_linked_doc(doc) is True:
|
||||
frappe.publish_progress(percent=i * 100 / len(docs), title=_("Cancelling documents"))
|
||||
linked_doc = frappe.get_doc(doc.get("doctype"), doc.get("name"))
|
||||
linked_doc.cancel()
|
||||
|
||||
|
||||
def validate_linked_doc(docinfo):
|
||||
"""
|
||||
Validate a document to be submitted and non-exempted from auto-cancel.
|
||||
|
||||
Args:
|
||||
docs (dict): The document to check for submitted and non-exempt from auto-cancel
|
||||
|
||||
Returns:
|
||||
bool: True if linked document passes all validations, else False
|
||||
"""
|
||||
|
||||
# skip non-submittable doctypes since they don't need to be cancelled
|
||||
if not frappe.get_meta(docinfo.get('doctype')).is_submittable:
|
||||
return False
|
||||
|
||||
# skip draft or cancelled documents
|
||||
if docinfo.get('docstatus') != 1:
|
||||
return False
|
||||
|
||||
# skip other doctypes since they don't need to be cancelled
|
||||
auto_cancel_exempt_doctypes = get_exempted_doctypes()
|
||||
if docinfo.get('doctype') in auto_cancel_exempt_doctypes:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_exempted_doctypes():
|
||||
""" Get list of doctypes exempted from being auto-cancelled """
|
||||
|
||||
auto_cancel_exempt_doctypes = []
|
||||
for doctypes in frappe.get_hooks('auto_cancel_exempted_doctypes'):
|
||||
auto_cancel_exempt_doctypes.append(doctypes)
|
||||
return auto_cancel_exempt_doctypes
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -184,8 +289,8 @@ def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=F
|
|||
if is_single(df.doctype): continue
|
||||
|
||||
# optimized to get both link exists and parenttype
|
||||
possible_link = frappe.db.sql("""select distinct `{doctype_fieldname}`, parenttype
|
||||
from `tab{doctype}` where `{doctype_fieldname}`=%s""".format(**df), doctype, as_dict=True)
|
||||
possible_link = frappe.get_all(df.doctype, filters={df.doctype_fieldname: doctype},
|
||||
fields=['parenttype'], distinct=True)
|
||||
|
||||
if not possible_link: continue
|
||||
|
||||
|
|
@ -203,4 +308,4 @@ def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=F
|
|||
"doctype_fieldname": df.doctype_fieldname
|
||||
}
|
||||
|
||||
return ret
|
||||
return ret
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from frappe.model.utils.user_settings import get_user_settings
|
|||
from frappe.permissions import get_doc_permissions
|
||||
from frappe.desk.form.document_follow import is_document_followed
|
||||
from frappe import _
|
||||
from six.moves.urllib.parse import quote
|
||||
|
||||
@frappe.whitelist()
|
||||
def getdoc(doctype, name, user=None):
|
||||
|
|
@ -101,7 +102,8 @@ def get_docinfo(doc=None, doctype=None, name=None):
|
|||
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
|
||||
"milestones": get_milestones(doc.doctype, doc.name),
|
||||
"is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user),
|
||||
"tags": get_tags(doc.doctype, doc.name)
|
||||
"tags": get_tags(doc.doctype, doc.name),
|
||||
"document_email": get_document_email(doc.doctype, doc.name)
|
||||
}
|
||||
|
||||
def get_milestones(doctype, name):
|
||||
|
|
@ -263,4 +265,15 @@ def get_tags(doctype, name):
|
|||
"document_name": name
|
||||
}, fields=["tag"])]
|
||||
|
||||
return ",".join([tag for tag in tags])
|
||||
return ",".join(tags)
|
||||
|
||||
def get_document_email(doctype, name):
|
||||
email = get_automatic_email_link()
|
||||
if not email:
|
||||
return None
|
||||
|
||||
email = email.split("@")
|
||||
return "{0}+{1}+{2}@{3}".format(email[0], quote(doctype), quote(name), email[1])
|
||||
|
||||
def get_automatic_email_link():
|
||||
return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id")
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class Leaderboard {
|
|||
constructor(parent) {
|
||||
frappe.ui.make_app_page({
|
||||
parent: parent,
|
||||
title: "Leaderboard",
|
||||
title: __("Leaderboard"),
|
||||
single_column: false
|
||||
});
|
||||
this.parent = parent;
|
||||
|
|
@ -187,7 +187,7 @@ class Leaderboard {
|
|||
render_search_box() {
|
||||
|
||||
this.$search_box =
|
||||
$(`<div class="leaderboard-search col-md-3">
|
||||
$(`<div class="leaderboard-search form-group col-md-3">
|
||||
<input type="text" placeholder="Search" class="form-control leaderboard-search-input input-sm">
|
||||
</div>`);
|
||||
|
||||
|
|
@ -363,7 +363,7 @@ class Leaderboard {
|
|||
|
||||
const link = `#Form/${this.options.selected_doctype}/${item.name}`;
|
||||
const name_html = item.formatted_name ?
|
||||
`<span class="text-muted ellipsis">${item.formatted_name}</span>`
|
||||
`<span class="text-muted ellipsis list-id">${item.formatted_name}</span>`
|
||||
: `<a class="grey list-id ellipsis" href="${link}"> ${item.name} </a>`;
|
||||
const html =
|
||||
`<div class="list-item">
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ def get_script(report_name):
|
|||
|
||||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
def run(report_name, filters=None, user=None):
|
||||
def run(report_name, filters=None, user=None, ignore_prepared_report=False):
|
||||
|
||||
report = get_report_doc(report_name)
|
||||
if not user:
|
||||
|
|
@ -169,7 +169,7 @@ def run(report_name, filters=None, user=None):
|
|||
|
||||
result = None
|
||||
|
||||
if report.prepared_report and not report.disable_prepared_report:
|
||||
if report.prepared_report and not report.disable_prepared_report and not ignore_prepared_report:
|
||||
if filters:
|
||||
if isinstance(filters, string_types):
|
||||
filters = json.loads(filters)
|
||||
|
|
@ -229,7 +229,7 @@ def get_prepared_report_result(report, filters, dn="", user=None):
|
|||
"status": "Completed",
|
||||
"filters": json.dumps(filters),
|
||||
"owner": user,
|
||||
"report_name": report.custom_report or report.report_name
|
||||
"report_name": report.get('custom_report') or report.get('report_name')
|
||||
},
|
||||
order_by = 'creation desc'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ class AutoEmailReport(Document):
|
|||
self.prepare_dynamic_filters()
|
||||
|
||||
columns, data = report.get_data(limit=self.no_of_rows or 100, user = self.user,
|
||||
filters = self.filters, as_dict=True)
|
||||
filters = self.filters, as_dict=True, ignore_prepared_report=True)
|
||||
|
||||
# add serial numbers
|
||||
columns.insert(0, frappe._dict(fieldname='idx', label='', width='30px'))
|
||||
|
|
|
|||
|
|
@ -765,7 +765,3 @@ def get_max_email_uid(email_account):
|
|||
else:
|
||||
max_uid = cint(result[0].get("uid", 0)) + 1
|
||||
return max_uid
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_automatic_email_link():
|
||||
return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id")
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ doc_events = {
|
|||
"on_change": [
|
||||
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points"
|
||||
],
|
||||
"after_insert": "frappe.cache_manager.build_table_count_cache",
|
||||
},
|
||||
"Event": {
|
||||
"after_insert": "frappe.integrations.doctype.google_calendar.google_calendar.insert_event_in_google_calendar",
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class StripeSettings(Document):
|
|||
def create_charge_on_stripe(self):
|
||||
import stripe
|
||||
try:
|
||||
charge = stripe.Charge.create(amount=cint(flt(self.data.amount)*100), currency=self.data.currency, source=self.data.stripe_token_id, description=self.data.description)
|
||||
charge = stripe.Charge.create(amount=cint(flt(self.data.amount)*100), currency=self.data.currency, source=self.data.stripe_token_id, description=self.data.description, receipt_email=self.data.payer_email)
|
||||
|
||||
if charge.captured == True:
|
||||
self.integration_request.db_set('status', 'Completed', update_modified=False)
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@ frappe.ui.form.on('Webhook', {
|
|||
|
||||
webhook_doctype: (frm) => {
|
||||
frappe.webhook.set_fieldname_select(frm);
|
||||
},
|
||||
|
||||
enable_security: (frm) => {
|
||||
frm.toggle_reqd('webhook_secret', frm.doc.enable_security);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@
|
|||
"request_url",
|
||||
"cb_webhook",
|
||||
"request_structure",
|
||||
"sb_security",
|
||||
"enable_security",
|
||||
"webhook_secret",
|
||||
"sb_webhook_headers",
|
||||
"webhook_headers",
|
||||
"sb_webhook_data",
|
||||
|
|
@ -127,10 +130,27 @@
|
|||
"fieldtype": "Select",
|
||||
"label": "Naming Series",
|
||||
"options": "\nHOOK-.####"
|
||||
},
|
||||
{
|
||||
"fieldname": "sb_security",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Webhook Security"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_security",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Security"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.enable_security == 1",
|
||||
"fieldname": "webhook_secret",
|
||||
"fieldtype": "Password",
|
||||
"label": "Webhook Secret"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-01-06 02:51:07.997566",
|
||||
"modified": "2020-01-13 01:53:04.459968",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Webhook",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
from time import sleep
|
||||
|
||||
|
|
@ -16,6 +19,8 @@ from frappe import _
|
|||
from frappe.model.document import Document
|
||||
from frappe.utils.jinja import validate_template
|
||||
|
||||
WEBHOOK_SECRET_HEADER = "X-Frappe-Webhook-Signature"
|
||||
|
||||
|
||||
class Webhook(Document):
|
||||
def validate(self):
|
||||
|
|
@ -94,10 +99,23 @@ def enqueue_webhook(doc, webhook):
|
|||
|
||||
def get_webhook_headers(doc, webhook):
|
||||
headers = {}
|
||||
|
||||
if webhook.enable_security:
|
||||
data = get_webhook_data(doc, webhook)
|
||||
signature = base64.b64encode(
|
||||
hmac.new(
|
||||
webhook.get_password("webhook_secret").encode("utf8"),
|
||||
json.dumps(data).encode("utf8"),
|
||||
hashlib.sha256
|
||||
).digest()
|
||||
)
|
||||
headers[WEBHOOK_SECRET_HEADER] = signature
|
||||
|
||||
if webhook.webhook_headers:
|
||||
for h in webhook.webhook_headers:
|
||||
if h.get("key") and h.get("value"):
|
||||
headers[h.get("key")] = h.get("value")
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ def login_via_office365(code, state):
|
|||
def login_via_salesforce(code, state):
|
||||
login_via_oauth2("salesforce", code, state, decoder=decoder_compat)
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def login_via_fairlogin(code, state):
|
||||
login_via_oauth2("fairlogin", code, state, decoder=decoder_compat)
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def custom(code, state):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -584,14 +584,17 @@ class Document(BaseDocument):
|
|||
|
||||
def get_permlevel_access(self, permission_type='write'):
|
||||
if not hasattr(self, "_has_access_to"):
|
||||
self._has_access_to = {}
|
||||
|
||||
if not self._has_access_to.get(permission_type):
|
||||
self._has_access_to[permission_type] = []
|
||||
roles = frappe.get_roles()
|
||||
self._has_access_to = []
|
||||
for perm in self.get_permissions():
|
||||
if perm.role in roles and perm.permlevel > 0 and perm.get(permission_type):
|
||||
if perm.permlevel not in self._has_access_to:
|
||||
self._has_access_to.append(perm.permlevel)
|
||||
if perm.permlevel not in self._has_access_to[permission_type]:
|
||||
self._has_access_to[permission_type].append(perm.permlevel)
|
||||
|
||||
return self._has_access_to
|
||||
return self._has_access_to[permission_type]
|
||||
|
||||
def has_permlevel_access_to(self, fieldname, df=None, permission_type='read'):
|
||||
if not df:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import frappe
|
|||
|
||||
def execute():
|
||||
if frappe.db.table_exists('Prepared Report'):
|
||||
frappe.reload_doc("core", "doctype", "prepared_doctype")
|
||||
frappe.reload_doc("core", "doctype", "prepared_report")
|
||||
prepared_reports = frappe.get_all("Prepared Report")
|
||||
for report in prepared_reports:
|
||||
frappe.delete_doc("Prepared Report", report.name)
|
||||
|
|
|
|||
|
|
@ -481,7 +481,7 @@ frappe.Application = Class.extend({
|
|||
// Iterate over changelog
|
||||
var change_log_dialog = frappe.msgprint({
|
||||
message: frappe.render_template("change_log", {"change_log": change_log}),
|
||||
title: __("Updated To New Version 🎉"),
|
||||
title: __("Updated To A New Version 🎉"),
|
||||
wide: true,
|
||||
scroll: true
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,7 +27,11 @@ frappe.ui.form.ControlBarcode = frappe.ui.form.ControlData.extend({
|
|||
set_formatted_input(value) {
|
||||
// Set values to display
|
||||
let svg = value;
|
||||
const barcode_value = $(svg).attr('data-barcode-value');
|
||||
let barcode_value = '';
|
||||
|
||||
if (value && value.startsWith('<svg')) {
|
||||
barcode_value = $(svg).attr('data-barcode-value');
|
||||
}
|
||||
|
||||
if (!barcode_value && this.doc) {
|
||||
svg = this.get_barcode_html(value);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlCode.extend({
|
||||
frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({
|
||||
horizontal: false,
|
||||
|
||||
make_wrapper() {
|
||||
// Create the elements for map area
|
||||
this._super();
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
var me = this;
|
||||
this.wrapper = $(frappe.render_template("timeline",{doctype: me.frm.doctype,allow_events_in_timeline: me.frm.meta.allow_events_in_timeline})).appendTo(me.parent);
|
||||
|
||||
this.set_automatic_link_email();
|
||||
this.display_automatic_link_email();
|
||||
this.list = this.wrapper.find(".timeline-items");
|
||||
this.email_link = this.wrapper.find(".timeline-email-import");
|
||||
|
||||
|
|
@ -117,31 +117,12 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
});
|
||||
}
|
||||
|
||||
set_automatic_link_email() {
|
||||
if (!frappe.email.automatic_link_email){
|
||||
frappe.call("frappe.email.doctype.email_account.email_account.get_automatic_email_link").then((r) => {
|
||||
if (r && r.message) {
|
||||
frappe.email.automatic_link_email = r.message;
|
||||
} else {
|
||||
frappe.email.automatic_link_email = null;
|
||||
}
|
||||
this.display_automatic_link_email();
|
||||
});
|
||||
} else {
|
||||
this.display_automatic_link_email();
|
||||
}
|
||||
}
|
||||
|
||||
display_automatic_link_email() {
|
||||
var me = this;
|
||||
if (frappe.email.automatic_link_email){
|
||||
let email_id = frappe.email.automatic_link_email;
|
||||
email_id = email_id.split("@")[0] +"+"+ encodeURIComponent(me.frm.doctype) +"+"+ encodeURIComponent(me.frm.docname)
|
||||
+"@"+ email_id.split("@")[1];
|
||||
let docinfo = this.frm.get_docinfo();
|
||||
|
||||
$(".timeline-email-import-link").text(email_id);
|
||||
} else {
|
||||
$('.timeline-email-import').addClass("hide");
|
||||
if (docinfo.document_email){
|
||||
let link = __("Send an email to {0} to link it here", [`<b><a class="timeline-email-import-link copy-to-clipboard">${docinfo.document_email}</a></b>`]);
|
||||
$('.timeline-email-import').html(link);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -211,7 +192,7 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
me.render_timeline_item(d);
|
||||
});
|
||||
|
||||
|
||||
me.display_automatic_link_email();
|
||||
|
||||
// more btn
|
||||
if (this.more===undefined && timeline.length===20) {
|
||||
|
|
@ -371,6 +352,10 @@ frappe.ui.form.Timeline = class Timeline {
|
|||
c.sender = c.sender.split("<")[1].split(">")[0];
|
||||
}
|
||||
|
||||
if (!c.doctype && ['Comment', 'Communication'].includes(c.communication_type)) {
|
||||
c.doctype = c.communication_type;
|
||||
}
|
||||
|
||||
c.user_info = frappe.user_info(c.sender);
|
||||
|
||||
c["delete"] = "";
|
||||
|
|
|
|||
|
|
@ -648,18 +648,96 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
}
|
||||
|
||||
savecancel(btn, callback, on_error) {
|
||||
var me = this;
|
||||
|
||||
const me = this;
|
||||
this.validate_form_action('Cancel');
|
||||
frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), function() {
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.desk.form.linked_with.get_submitted_linked_docs",
|
||||
args: {
|
||||
doctype: me.doc.doctype,
|
||||
name: me.doc.name
|
||||
},
|
||||
freeze: true,
|
||||
callback: (r) => {
|
||||
if (!r.exc && r.message.count > 0) {
|
||||
me._cancel_all(r, btn, callback, on_error);
|
||||
} else {
|
||||
me._cancel(btn, callback, on_error, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_cancel_all(r, btn, callback, on_error) {
|
||||
const me = this;
|
||||
|
||||
// add confirmation message for cancelling all linked docs
|
||||
let links_text = "";
|
||||
let links = r.message.docs;
|
||||
const doctypes = Array.from(new Set(links.map(link => link.doctype)));
|
||||
|
||||
for (let doctype of doctypes) {
|
||||
let docnames = links
|
||||
.filter((link) => link.doctype == doctype)
|
||||
.map((link) => frappe.utils.get_form_link(link.doctype, link.name, true))
|
||||
.join(", ");
|
||||
links_text += `<li><strong>${doctype}</strong>: ${docnames}</li>`;
|
||||
}
|
||||
links_text = `<ul>${links_text}</ul>`;
|
||||
|
||||
let confirm_message = __('{0} {1} is linked with the following submitted documents: {2}',
|
||||
[(me.doc.doctype).bold(), me.doc.name, links_text]);
|
||||
|
||||
let can_cancel = links.every((link) => frappe.model.can_cancel(link.doctype));
|
||||
if (can_cancel) {
|
||||
confirm_message += __('Do you want to cancel all linked documents?');
|
||||
} else {
|
||||
confirm_message += __('You do not have permissions to cancel all linked documents.');
|
||||
}
|
||||
|
||||
// generate dialog box to cancel all linked docs
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Cancel All Documents"),
|
||||
fields: [{
|
||||
fieldtype: "HTML",
|
||||
options: `<p class="frappe-confirm-message">${confirm_message}</p>`
|
||||
}]
|
||||
}, () => me.handle_save_fail(btn, on_error));
|
||||
|
||||
// if user can cancel all linked docs, add action to the dialog
|
||||
if (can_cancel) {
|
||||
d.set_primary_action("Cancel All", () => {
|
||||
d.hide();
|
||||
frappe.call({
|
||||
method: "frappe.desk.form.linked_with.cancel_all_linked_docs",
|
||||
args: {
|
||||
docs: links
|
||||
},
|
||||
freeze: true,
|
||||
callback: (resp) => {
|
||||
if (!resp.exc) {
|
||||
me.reload_doc();
|
||||
me._cancel(btn, callback, on_error, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
d.show();
|
||||
};
|
||||
|
||||
_cancel(btn, callback, on_error, skip_confirm) {
|
||||
const me = this;
|
||||
const cancel_doc = () => {
|
||||
frappe.validated = true;
|
||||
me.script_manager.trigger("before_cancel").then(function() {
|
||||
if(!frappe.validated) {
|
||||
me.script_manager.trigger("before_cancel").then(() => {
|
||||
if (!frappe.validated) {
|
||||
return me.handle_save_fail(btn, on_error);
|
||||
}
|
||||
|
||||
var after_cancel = function(r) {
|
||||
if(r.exc) {
|
||||
if (r.exc) {
|
||||
me.handle_save_fail(btn, on_error);
|
||||
} else {
|
||||
frappe.utils.play_sound("cancel");
|
||||
|
|
@ -670,8 +748,14 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
};
|
||||
frappe.ui.form.save(me, "cancel", after_cancel, btn);
|
||||
});
|
||||
}, () => me.handle_save_fail(btn, on_error));
|
||||
}
|
||||
}
|
||||
|
||||
if (skip_confirm) {
|
||||
cancel_doc();
|
||||
} else {
|
||||
frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), cancel_doc, me.handle_save_fail(btn, on_error));
|
||||
}
|
||||
};
|
||||
|
||||
savetrash() {
|
||||
this.validate_form_action("Delete");
|
||||
|
|
|
|||
|
|
@ -65,26 +65,22 @@ export default class Grid {
|
|||
<div class="small form-clickable-section grid-footer">
|
||||
<div class="row">
|
||||
<div class="col-sm-5 grid-buttons">
|
||||
<button type="reset"
|
||||
class="btn btn-xs btn-danger grid-remove-rows hidden"
|
||||
<button class="btn btn-xs btn-danger grid-remove-rows hidden"
|
||||
style="margin-right: 4px;"
|
||||
data-action="delete_rows">
|
||||
${__("Delete")}
|
||||
</button>
|
||||
<button type="reset"
|
||||
class="btn btn-xs btn-danger grid-remove-all-rows hidden"
|
||||
<button class="btn btn-xs btn-danger grid-remove-all-rows hidden"
|
||||
style="margin-right: 4px;"
|
||||
data-action="delete_all_rows">
|
||||
${__("Delete All")}
|
||||
</button>
|
||||
<button type="reset"
|
||||
class="grid-add-multiple-rows btn btn-xs btn-default hidden"
|
||||
<button class="grid-add-multiple-rows btn btn-xs btn-default hidden"
|
||||
style="margin-right: 4px;">
|
||||
${__("Add Multiple")}</a>
|
||||
</button>
|
||||
<!-- hack to allow firefox include this in tabs -->
|
||||
<button type="reset"
|
||||
class="btn btn-xs btn-default grid-add-row">
|
||||
<button class="btn btn-xs btn-default grid-add-row">
|
||||
${__("Add Row")}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,9 +18,7 @@
|
|||
{% } %}
|
||||
</div>
|
||||
<div class="timeline-new-email timeline-email-import text-muted small">
|
||||
<p>
|
||||
{%= __("Send an email to {0} to link it here.", [`<b><a class="timeline-email-import-link copy-to-clipboard"></a></b>`]) %}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div class="timeline-items">
|
||||
|
||||
|
|
|
|||
|
|
@ -267,19 +267,24 @@ frappe.ui.form.Toolbar = Class.extend({
|
|||
});
|
||||
}
|
||||
|
||||
if(frappe.user_roles.includes("System Manager") && me.frm.meta.issingle === 0) {
|
||||
this.page.add_menu_item(__("Customize"), function() {
|
||||
if (frappe.user_roles.includes("System Manager") && me.frm.meta.issingle === 0) {
|
||||
let is_doctype_form = me.frm.doctype === 'DocType';
|
||||
let doctype = is_doctype_form ? me.frm.docname : me.frm.doctype;
|
||||
let is_doctype_custom = is_doctype_form ? me.frm.doc.custom : false;
|
||||
|
||||
if (me.frm.meta && me.frm.meta.custom) {
|
||||
frappe.set_route('Form', 'DocType', me.frm.doctype);
|
||||
} else {
|
||||
frappe.set_route('Form', 'Customize Form', {
|
||||
doc_type: me.frm.doctype
|
||||
});
|
||||
}
|
||||
}, true);
|
||||
if (doctype != 'DocType' && !is_doctype_custom) {
|
||||
this.page.add_menu_item(__("Customize"), function() {
|
||||
if (me.frm.meta && me.frm.meta.custom) {
|
||||
frappe.set_route('Form', 'DocType', doctype);
|
||||
} else {
|
||||
frappe.set_route('Form', 'Customize Form', {
|
||||
doc_type: doctype
|
||||
});
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
if (frappe.boot.developer_mode===1) {
|
||||
if (frappe.boot.developer_mode===1 && !is_doctype_form) {
|
||||
// edit doctype
|
||||
this.page.add_menu_item(__("Edit DocType"), function() {
|
||||
frappe.set_route('Form', 'DocType', me.frm.doctype);
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ frappe.ui.Filter = class {
|
|||
// called when condition is changed,
|
||||
// don't change if all is well
|
||||
if(this.field && cur.fieldname == fieldname && df.fieldtype == cur.fieldtype &&
|
||||
df.parent == cur.parent) {
|
||||
df.parent == cur.parent && df.options == cur.options) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,11 @@ frappe.ui.GroupBy = class {
|
|||
}
|
||||
|
||||
apply_settings(settings) {
|
||||
this.groupby_select.val(settings.group_by);
|
||||
|
||||
// Extract fieldname from `tabdoctype`.`fieldname`
|
||||
let group_by_fieldname = settings.group_by.split('.')[1].replace(/`/g, '');
|
||||
|
||||
this.groupby_select.val(group_by_fieldname);
|
||||
this.aggregate_function_select.val(settings.aggregate_function);
|
||||
this.show_hide_aggregate_on();
|
||||
this.aggregate_on_select.val(settings.aggregate_on);
|
||||
|
|
|
|||
|
|
@ -538,7 +538,7 @@ frappe.provide("frappe.views");
|
|||
if(!card) return;
|
||||
make_dom();
|
||||
render_card_meta();
|
||||
bind_edit_card();
|
||||
add_task_link();
|
||||
// edit_card_title();
|
||||
}
|
||||
|
||||
|
|
@ -576,11 +576,9 @@ frappe.provide("frappe.views");
|
|||
self.$card.find(".kanban-card-meta").empty().append(html);
|
||||
}
|
||||
|
||||
function bind_edit_card() {
|
||||
self.$card.find('.kanban-card.content').on('click', function() {
|
||||
frappe.set_route('Form', card.doctype, card.name);
|
||||
// setup_edit_card();
|
||||
});
|
||||
function add_task_link() {
|
||||
let taskLink = frappe.utils.get_form_link(card.doctype, card.name);
|
||||
self.$card.find('.kanban-card-redirect').attr('href', taskLink);
|
||||
}
|
||||
|
||||
function refresh_dialog() {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
<div class="kanban-card-wrapper {{ disable_click }}" data-name="{{name}}">
|
||||
<div class="kanban-card content">
|
||||
<div class="kanban-card-title">
|
||||
{{ title }}
|
||||
<a class="kanban-card-redirect" href="#">
|
||||
<div class="kanban-card content">
|
||||
<div class="kanban-card-title">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="kanban-card-meta">
|
||||
</div>
|
||||
</div>
|
||||
<div class="kanban-card-meta">
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -29,13 +29,13 @@
|
|||
<!-- body -->
|
||||
<tbody>
|
||||
{% for row in data %}
|
||||
<tr>
|
||||
<tr style="height: 30px">
|
||||
{% for col in columns %}
|
||||
{% if col.name && col._id !== "_check" %}
|
||||
|
||||
{% var value = col.fieldname ? row[col.fieldname] : row[col.id]; %}
|
||||
|
||||
<td>
|
||||
<td {% if row.bold == 1 %} style="font-weight: bold" {% endif %}>
|
||||
<span {% if col._index == 0 %} style="padding-left: {%= cint(row.indent) * 2 %}em" {% endif %}>
|
||||
{{
|
||||
col.formatter
|
||||
|
|
|
|||
|
|
@ -684,7 +684,6 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
}
|
||||
|
||||
build_fields() {
|
||||
this.fields.push(['docstatus', this.doctype]);
|
||||
super.build_fields();
|
||||
}
|
||||
|
||||
|
|
@ -742,6 +741,16 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
}
|
||||
}
|
||||
|
||||
add_status_dependency_column(col, doctype) {
|
||||
// Adds dependent column from which status is derived if required
|
||||
if (!this.fields.find(f => f[0] === col)) {
|
||||
const field = [col, doctype];
|
||||
this.fields.push(field);
|
||||
this.refresh();
|
||||
frappe.show_alert(__('Also adding the status dependency field {0}', [field[0].bold()]));
|
||||
}
|
||||
}
|
||||
|
||||
remove_column_from_datatable(column) {
|
||||
const index = this.fields.findIndex(f => column.field === f[0]);
|
||||
if (index === -1) return;
|
||||
|
|
@ -778,12 +787,24 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
|
||||
let doctype_fields = frappe.meta.get_docfields(this.doctype).filter(standard_fields_filter);
|
||||
|
||||
// filter out docstatus field from picker
|
||||
let std_fields = frappe.model.std_fields.filter( df => df.fieldname !== 'docstatus');
|
||||
|
||||
// add status field derived from docstatus, if status is not a standard field
|
||||
if (!frappe.meta.has_field(this.doctype, 'status')) {
|
||||
doctype_fields = [{
|
||||
label: __('Status'),
|
||||
fieldname: 'docstatus',
|
||||
fieldtype: 'Data'
|
||||
}].concat(doctype_fields);
|
||||
}
|
||||
|
||||
doctype_fields = [{
|
||||
label: __('ID'),
|
||||
fieldname: 'name',
|
||||
fieldtype: 'Data',
|
||||
reqd: 1
|
||||
}].concat(doctype_fields, frappe.model.std_fields);
|
||||
}].concat(doctype_fields, std_fields);
|
||||
|
||||
out[this.doctype] = doctype_fields;
|
||||
|
||||
|
|
@ -858,16 +879,23 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
this.columns_map = {};
|
||||
|
||||
for (let f of this.fields) {
|
||||
let column;
|
||||
if (f[0]!=='docstatus') {
|
||||
let column = this.build_column(f);
|
||||
if (column) {
|
||||
if (column_widths) {
|
||||
column.width = column_widths[column.id] || column.width || 120;
|
||||
}
|
||||
this.columns.push(column);
|
||||
this.columns_map[column.id] = column;
|
||||
column = this.build_column(f);
|
||||
} else {
|
||||
// if status is not in fields append status column derived from docstatus
|
||||
if (!this.fields.includes(['status', this.doctype]) && !frappe.meta.has_field(this.doctype, 'status')) {
|
||||
column = this.build_column(['docstatus', this.doctype]);
|
||||
}
|
||||
}
|
||||
|
||||
if (column) {
|
||||
if (column_widths) {
|
||||
column.width = column_widths[column.id] || column.width || 120;
|
||||
}
|
||||
this.columns.push(column);
|
||||
this.columns_map[column.id] = column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -892,9 +920,14 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
}
|
||||
}
|
||||
docfield.parent = this.doctype;
|
||||
if (fieldname == "name") {
|
||||
if (fieldname == 'name') {
|
||||
docfield.options = this.doctype;
|
||||
}
|
||||
if (fieldname == 'docstatus' && !frappe.meta.has_field(this.doctype, 'status')) {
|
||||
docfield.label = 'Status';
|
||||
docfield.fieldtype = 'Data';
|
||||
docfield.name = 'status';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!docfield) return;
|
||||
|
|
@ -982,7 +1015,6 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
totals_row[0].content = __('Totals').bold();
|
||||
out.push(totals_row);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
|
|
@ -1003,8 +1035,22 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (col.field in d) {
|
||||
if (col.field === 'docstatus' && !frappe.meta.has_field(this.doctype, 'status')) {
|
||||
// get status from docstatus
|
||||
let status = frappe.get_indicator(d, this.doctype);
|
||||
if (!status[0]) {
|
||||
// get_indicator returns the dependent field's condition as the 3rd parameter
|
||||
let dependent_col = status[2].split(',')[0];
|
||||
// add status dependency column
|
||||
this.add_status_dependency_column(dependent_col, this.doctype);
|
||||
}
|
||||
return {
|
||||
name: d.name,
|
||||
doctype: col.docfield.parent,
|
||||
content: status[0],
|
||||
editable: false
|
||||
};
|
||||
} else if (col.field in d) {
|
||||
const value = d[col.field];
|
||||
return {
|
||||
name: d.name,
|
||||
|
|
|
|||
|
|
@ -86,7 +86,11 @@ export default class WebForm extends frappe.ui.FieldGroup {
|
|||
}
|
||||
|
||||
setup_delete_button() {
|
||||
this.add_button_to_header("Delete", "danger", () => this.delete());
|
||||
this.add_button_to_header(
|
||||
'<i class="fa fa-trash" aria-hidden="true"></i>',
|
||||
"light",
|
||||
() => this.delete()
|
||||
);
|
||||
}
|
||||
|
||||
setup_print_button() {
|
||||
|
|
|
|||
|
|
@ -713,6 +713,7 @@ li.user-progress {
|
|||
height: 60px;
|
||||
width: 60px;
|
||||
background-color: #fafbfc;
|
||||
overflow: hidden;
|
||||
|
||||
.flex-text {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -87,6 +87,15 @@
|
|||
|
||||
.kanban-card-wrapper {
|
||||
position: relative;
|
||||
|
||||
.kanban-card-redirect {
|
||||
display: block;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-card:hover, .new-card-area, .edit-card-area {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
{{ render_table(df, doc) }}
|
||||
{%- elif df.fieldtype=="HTML" and df.options -%}
|
||||
<div>{{ frappe.render_template(df.options, {"doc": doc}) or "" }}</div>
|
||||
{%- elif df.fieldtype in ("Text", "Text Editor", "Code") -%}
|
||||
{%- elif df.fieldtype in ("Text", "Text Editor", "Code", "Long Text") -%}
|
||||
{{ render_text_field(df, doc) }}
|
||||
{%- elif df.fieldtype in ("Image", "Attach Image", "Attach")
|
||||
and (guess_mimetype(doc[df.fieldname])[0] or "").startswith("image/") -%}
|
||||
|
|
@ -95,7 +95,7 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
|
|||
{%- macro render_text_field(df, doc) -%}
|
||||
{%- if doc.get(df.fieldname) != None -%}
|
||||
<div style="padding: 10px 0px" {{ fieldmeta(df) }}>
|
||||
{%- if df.fieldtype in ("Text", "Code") %}<label>{{ _(df.label) }}</label>{%- endif %}
|
||||
{%- if df.fieldtype in ("Text", "Code", "Long Text") %}<label>{{ _(df.label) }}</label>{%- endif %}
|
||||
{%- if df.fieldtype=="Code" %}
|
||||
<pre class="value">{{ doc.get(df.fieldname) }}</pre>
|
||||
{% else -%}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(),
|
|||
|
||||
xmloutput_fh = None
|
||||
if junit_xml_output:
|
||||
xmloutput_fh = open(junit_xml_output, 'w')
|
||||
xmloutput_fh = open(junit_xml_output, 'wb')
|
||||
unittest_runner = xmlrunner_wrapper(xmloutput_fh)
|
||||
else:
|
||||
unittest_runner = unittest.TextTestRunner
|
||||
|
|
@ -68,11 +68,11 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(),
|
|||
frappe.get_attr(fn)()
|
||||
|
||||
if doctype:
|
||||
ret = run_tests_for_doctype(doctype, verbose, tests, force, profile)
|
||||
ret = run_tests_for_doctype(doctype, verbose, tests, force, profile, junit_xml_output=junit_xml_output)
|
||||
elif module:
|
||||
ret = run_tests_for_module(module, verbose, tests, profile)
|
||||
ret = run_tests_for_module(module, verbose, tests, profile, junit_xml_output=junit_xml_output)
|
||||
else:
|
||||
ret = run_all_tests(app, verbose, profile, ui_tests, failfast=failfast)
|
||||
ret = run_all_tests(app, verbose, profile, ui_tests, failfast=failfast, junit_xml_output=junit_xml_output)
|
||||
|
||||
if frappe.db: frappe.db.commit()
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ class TimeLoggingTestResult(unittest.TextTestResult):
|
|||
super(TimeLoggingTestResult, self).addSuccess(test)
|
||||
|
||||
|
||||
def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfast=False):
|
||||
def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfast=False, junit_xml_output=False):
|
||||
import os
|
||||
|
||||
apps = [app] if app else frappe.get_installed_apps()
|
||||
|
|
@ -130,11 +130,16 @@ def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfa
|
|||
_add_test(app, path, filename, verbose,
|
||||
test_suite, ui_tests)
|
||||
|
||||
if junit_xml_output:
|
||||
runner = unittest_runner(verbosity=1+(verbose and 1 or 0), failfast=failfast)
|
||||
else:
|
||||
runner = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0), failfast=failfast)
|
||||
|
||||
if profile:
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
||||
out = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0), failfast=failfast).run(test_suite)
|
||||
out = runner.run(test_suite)
|
||||
|
||||
if profile:
|
||||
pr.disable()
|
||||
|
|
@ -145,7 +150,7 @@ def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfa
|
|||
|
||||
return out
|
||||
|
||||
def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profile=False):
|
||||
def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profile=False, junit_xml_output=False):
|
||||
modules = []
|
||||
if not isinstance(doctypes, (list, tuple)):
|
||||
doctypes = [doctypes]
|
||||
|
|
@ -163,17 +168,17 @@ def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profil
|
|||
make_test_records(doctype, verbose=verbose, force=force)
|
||||
modules.append(importlib.import_module(test_module))
|
||||
|
||||
return _run_unittest(modules, verbose=verbose, tests=tests, profile=profile)
|
||||
return _run_unittest(modules, verbose=verbose, tests=tests, profile=profile, junit_xml_output=junit_xml_output)
|
||||
|
||||
def run_tests_for_module(module, verbose=False, tests=(), profile=False):
|
||||
def run_tests_for_module(module, verbose=False, tests=(), profile=False, junit_xml_output=False):
|
||||
module = importlib.import_module(module)
|
||||
if hasattr(module, "test_dependencies"):
|
||||
for doctype in module.test_dependencies:
|
||||
make_test_records(doctype, verbose=verbose)
|
||||
|
||||
return _run_unittest(module, verbose=verbose, tests=tests, profile=profile)
|
||||
return _run_unittest(module, verbose=verbose, tests=tests, profile=profile, junit_xml_output=junit_xml_output)
|
||||
|
||||
def _run_unittest(modules, verbose=False, tests=(), profile=False):
|
||||
def _run_unittest(modules, verbose=False, tests=(), profile=False, junit_xml_output=False):
|
||||
test_suite = unittest.TestSuite()
|
||||
|
||||
if not isinstance(modules, (list, tuple)):
|
||||
|
|
@ -189,13 +194,18 @@ def _run_unittest(modules, verbose=False, tests=(), profile=False):
|
|||
else:
|
||||
test_suite.addTest(module_test_cases)
|
||||
|
||||
if junit_xml_output:
|
||||
runner = unittest_runner(verbosity=1+(verbose and 1 or 0))
|
||||
else:
|
||||
runner = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0))
|
||||
|
||||
if profile:
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
||||
frappe.flags.tests_verbose = verbose
|
||||
|
||||
out = unittest_runner(verbosity=1+(verbose and 1 or 0)).run(test_suite)
|
||||
out = runner.run(test_suite)
|
||||
|
||||
|
||||
if profile:
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ from __future__ import unicode_literals
|
|||
|
||||
import frappe, unittest
|
||||
from frappe.desk.form.load import getdoctype, getdoc
|
||||
from frappe.core.page.permission_manager.permission_manager import update, reset
|
||||
from frappe.core.page.permission_manager.permission_manager import update, reset, add
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
|
||||
test_dependencies = ['Blog Category', 'Blogger']
|
||||
|
||||
class TestFormLoad(unittest.TestCase):
|
||||
def test_load(self):
|
||||
|
|
@ -20,56 +22,65 @@ class TestFormLoad(unittest.TestCase):
|
|||
self.assertTrue(meta.get("__calendar_js"))
|
||||
|
||||
def test_fieldlevel_permissions_in_load(self):
|
||||
blog = frappe.get_doc({
|
||||
"doctype": "Blog Post",
|
||||
"blog_category": "_Test Blog Category 1",
|
||||
"blog_intro": "Test Blog Intro",
|
||||
"blogger": "_Test Blogger 1",
|
||||
"content": "Test Blog Content",
|
||||
"title": "_Test Blog Post {}".format(frappe.utils.now()),
|
||||
"published": 0
|
||||
})
|
||||
|
||||
blog.insert()
|
||||
|
||||
user = frappe.get_doc('User', 'test@example.com')
|
||||
user.remove_roles('Website Manager')
|
||||
|
||||
user_roles = frappe.get_roles()
|
||||
user.remove_roles(*user_roles)
|
||||
user.add_roles('Blogger')
|
||||
|
||||
make_property_setter('Blog Post', 'published', 'permlevel', 1, 'Int')
|
||||
reset('Blog Post')
|
||||
|
||||
frappe.db.set_value('DocField', {
|
||||
'fieldname': 'published',
|
||||
'parent': 'Blog Post'
|
||||
}, 'permlevel', 1)
|
||||
|
||||
update('Blog Post', 'Website Manager', 0, 'permlevel', 1)
|
||||
add('Blog Post', 'Website Manager', 1)
|
||||
update('Blog Post', 'Website Manager', 1, 'write', 1)
|
||||
|
||||
frappe.set_user(user.name)
|
||||
|
||||
# print frappe.as_json(get_valid_perms('Blog Post'))
|
||||
blog_doc = get_blog(blog.name)
|
||||
|
||||
frappe.clear_cache(doctype='Blog Post')
|
||||
self.assertEqual(blog_doc.name, blog.name)
|
||||
# since published field has higher permlevel
|
||||
self.assertEqual(blog_doc.published, None)
|
||||
|
||||
blog = frappe.db.get_value('Blog Post', {'title': '_Test Blog Post'})
|
||||
|
||||
getdoc('Blog Post', blog)
|
||||
|
||||
checked = False
|
||||
|
||||
for doc in frappe.response.docs:
|
||||
if doc.name == blog:
|
||||
self.assertEqual(doc.published, None)
|
||||
checked = True
|
||||
|
||||
self.assertTrue(checked, True)
|
||||
|
||||
frappe.db.set_value('DocField', {
|
||||
'fieldname': 'published',
|
||||
'parent': 'Blog Post'
|
||||
}, 'permlevel', 0)
|
||||
|
||||
reset('Blog Post')
|
||||
|
||||
frappe.clear_cache(doctype='Blog Post')
|
||||
|
||||
frappe.response.docs = []
|
||||
getdoc('Blog Post', blog)
|
||||
|
||||
checked = False
|
||||
|
||||
for doc in frappe.response.docs:
|
||||
if doc.name == blog:
|
||||
self.assertEqual(doc.published, 1)
|
||||
checked = True
|
||||
|
||||
self.assertTrue(checked, True)
|
||||
# this will be ignored because user does not
|
||||
# have write access on `published` field (or on permlevel 1 fields)
|
||||
blog_doc.published = 1
|
||||
blog_doc.save()
|
||||
# since published field has higher permlevel
|
||||
self.assertEqual(blog_doc.published, 0)
|
||||
|
||||
frappe.set_user('Administrator')
|
||||
user.add_roles('Website Manager')
|
||||
frappe.set_user(user.name)
|
||||
|
||||
doc = frappe.get_doc('Blog Post', blog.name)
|
||||
doc.published = 1
|
||||
doc.save()
|
||||
|
||||
blog_doc = get_blog(blog.name)
|
||||
# now user should be allowed to read field with higher permlevel
|
||||
# (after adding Website Manager role)
|
||||
self.assertEqual(blog_doc.published, 1)
|
||||
|
||||
frappe.set_user('Administrator')
|
||||
|
||||
# reset user roles
|
||||
user.remove_roles('Blogger', 'Website Manager')
|
||||
user.add_roles(*user_roles)
|
||||
|
||||
def get_blog(blog_name):
|
||||
frappe.response.docs = []
|
||||
getdoc('Blog Post', blog_name)
|
||||
doc = frappe.response.docs[0]
|
||||
return doc
|
||||
|
|
@ -4046,3 +4046,5 @@ apps/frappe/frappe/public/js/frappe/web_form/web_form.js,Saved Successfully,保
|
|||
apps/frappe/frappe/core/doctype/user/user.py,Please ask your administrator to verify your sign-up,请向管理员询问,以确认您的注册
|
||||
DocType: Domain Settings,Active Domains,活动域
|
||||
apps/frappe/frappe/public/js/integrations/razorpay.js,Show Log,显示日志
|
||||
apps/frappe/frappe/desk/page/leaderboard/leaderboard.js,Leaderboard,排行榜
|
||||
apps/frappe/frappe/config/desktop.py,Leaderboard,排行榜
|
||||
|
|
|
|||
|
|
|
@ -256,6 +256,10 @@ app_license = "{app_license}"
|
|||
# "Task": "{app_name}.task.get_dashboard_data"
|
||||
# }}
|
||||
|
||||
# exempt linked doctypes from being automatically cancelled
|
||||
#
|
||||
# auto_cancel_exempted_doctypes = ["Auto Repeat"]
|
||||
|
||||
"""
|
||||
|
||||
desktop_template = """# -*- coding: utf-8 -*-
|
||||
|
|
|
|||
|
|
@ -375,7 +375,8 @@ $.extend(frappe, {
|
|||
|
||||
window.valid_email = function(id) {
|
||||
// eslint-disable-next-line
|
||||
return /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/.test(id.toLowerCase());
|
||||
// copied regex from frappe/utils.js validate_type
|
||||
return /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/.test(id.toLowerCase());
|
||||
}
|
||||
|
||||
window.validate_email = valid_email;
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ google-auth-httplib2==0.0.3
|
|||
google-auth-oauthlib==0.4.1
|
||||
google-auth==1.7.1
|
||||
googlemaps==3.1.1
|
||||
gunicorn==19.9.0
|
||||
gunicorn==19.10.0
|
||||
html2text==2016.9.19
|
||||
ipython==5.8.0
|
||||
Jinja2==2.10.3
|
||||
markdown2==2.3.6
|
||||
markdown2==2.3.7
|
||||
maxminddb-geolite2==2018.703
|
||||
ndg-httpsclient==0.5.1
|
||||
num2words==0.5.5
|
||||
|
|
@ -32,7 +32,7 @@ oauthlib==3.1.0
|
|||
openpyxl==2.6.4
|
||||
passlib==1.7.1
|
||||
pdfkit==0.6.1
|
||||
Pillow==6.2.1
|
||||
Pillow==6.2.2
|
||||
premailer==3.6.1
|
||||
psycopg2-binary==2.8.4
|
||||
pyasn1==0.4.7
|
||||
|
|
@ -46,7 +46,7 @@ pypng==0.0.20
|
|||
PyQRCode==1.2.1
|
||||
python-dateutil==2.8.1
|
||||
pytz==2019.3
|
||||
PyYAML==3.13
|
||||
PyYAML==5.1
|
||||
rauth==0.7.3
|
||||
redis>=3.0
|
||||
requests-oauthlib==1.3.0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue