Merge branch 'develop' of https://github.com/frappe/frappe into google_calender

This commit is contained in:
Himanshu Warekar 2019-07-20 22:05:05 +05:30
commit 47d738b4b3
181 changed files with 5074 additions and 7270 deletions

View file

@ -6,7 +6,7 @@ context('List View Settings', () => {
it('Default settings', () => {
cy.visit('/desk#List/DocType/List');
cy.get('.list-count').should('contain', "20 of");
cy.get('.sidebar-stat').should('contain', "No Tags");
cy.get('.sidebar-stat').should('contain', "Tags");
});
it('disable count and sidebar stats then verify', () => {
cy.visit('/desk#List/DocType/List');

View file

@ -0,0 +1,104 @@
// Copyright (c) 2018, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.provide("frappe.auto_repeat");
frappe.ui.form.on('Auto Repeat', {
setup: function(frm) {
frm.fields_dict['reference_doctype'].get_query = function() {
return {
query: "frappe.automation.doctype.auto_repeat.auto_repeat.get_auto_repeat_doctypes"
};
};
frm.fields_dict['reference_document'].get_query = function() {
return {
filters: {
"auto_repeat": ''
}
};
};
frm.fields_dict['print_format'].get_query = function() {
return {
filters: {
"doc_type": frm.doc.reference_doctype
}
};
};
},
refresh: function(frm) {
// auto repeat message
if (frm.is_new()) {
let customize_form_link = `<a href="#Form/Customize Form">${__('Customize Form')}</a>`;
frm.dashboard.set_headline(__('To configure Auto Repeat, enable "Allow Auto Repeat" from {0}.', [customize_form_link]));
}
// view document button
if (!frm.is_dirty()) {
let label = __('View {0}', [__(frm.doc.reference_doctype)]);
frm.add_custom_button(label, () =>
frappe.set_route("List", frm.doc.reference_doctype, { auto_repeat: frm.doc.name })
);
}
// auto repeat schedule
frappe.auto_repeat.render_schedule(frm);
},
template: function(frm) {
if (frm.doc.template) {
frappe.model.with_doc("Email Template", frm.doc.template, () => {
let email_template = frappe.get_doc("Email Template", frm.doc.template);
frm.set_value("subject", email_template.subject);
frm.set_value("message", email_template.response);
frm.refresh_field("subject");
frm.refresh_field("message");
});
}
},
get_contacts: function(frm) {
frm.call('fetch_linked_contacts');
},
preview_message: function(frm) {
if (frm.doc.message) {
frappe.call({
method: "frappe.automation.doctype.auto_repeat.auto_repeat.generate_message_preview",
args: {
reference_dt: frm.doc.reference_doctype,
reference_doc: frm.doc.reference_document,
subject: frm.doc.subject,
message: frm.doc.message
},
callback: function(r) {
if (r.message) {
frappe.msgprint(r.message.message, r.message.subject)
}
}
});
} else {
frappe.msgprint(__("Please setup a message first"), __("Message not setup"))
}
}
});
frappe.auto_repeat.render_schedule = function(frm) {
if (!frm.is_dirty() && frm.doc.status !== 'Disabled') {
frappe.call({
method: "get_auto_repeat_schedule",
doc: frm.doc
}).done((r) => {
frm.dashboard.wrapper.empty();
frm.dashboard.add_section(
frappe.render_template("auto_repeat_schedule", {
schedule_details : r.message || []
})
);
frm.dashboard.show();
});
} else {
frm.dashboard.hide();
}
};

View file

@ -0,0 +1,239 @@
{
"allow_import": 1,
"allow_rename": 1,
"autoname": "format:AUT-AR-{#####}",
"creation": "2018-03-09 11:22:31.192349",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"section_break_1",
"disabled",
"section_break_3",
"reference_doctype",
"reference_document",
"column_break_5",
"start_date",
"end_date",
"section_break_10",
"frequency",
"repeat_on_day",
"repeat_on_last_day",
"column_break_12",
"next_schedule_date",
"notification",
"notify_by_email",
"recipients",
"get_contacts",
"template",
"subject",
"message",
"preview_message",
"print_format",
"status"
],
"fields": [
{
"fieldname": "section_break_1",
"fieldtype": "Section Break"
},
{
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Reference Document Type",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "reference_document",
"fieldtype": "Dynamic Link",
"label": "Reference Document",
"no_copy": 1,
"options": "reference_doctype",
"reqd": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"default": "Today",
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date",
"reqd": 1
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "End Date"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled",
"no_copy": 1
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break"
},
{
"fieldname": "frequency",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Frequency",
"options": "\nDaily\nWeekly\nMonthly\nQuarterly\nHalf-yearly\nYearly",
"reqd": 1
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"depends_on": "eval: in_list([\"Monthly\", \"Quarterly\", \"Half-yearly\", \"Yearly\"], doc.frequency) && !doc.repeat_on_last_day\n",
"fieldname": "repeat_on_day",
"fieldtype": "Int",
"label": "Repeat on Day"
},
{
"fieldname": "next_schedule_date",
"fieldtype": "Date",
"label": "Next Schedule Date",
"no_copy": 1,
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
{
"collapsible": 1,
"fieldname": "notification",
"fieldtype": "Section Break",
"label": "Notification"
},
{
"default": "0",
"fieldname": "notify_by_email",
"fieldtype": "Check",
"label": "Notify by Email"
},
{
"depends_on": "notify_by_email",
"fieldname": "recipients",
"fieldtype": "Small Text",
"label": "Recipients"
},
{
"depends_on": "eval: doc.notify_by_email && doc.reference_doctype && doc.reference_document",
"fieldname": "get_contacts",
"fieldtype": "Button",
"label": "Get Contacts"
},
{
"depends_on": "eval: doc.notify_by_email",
"fieldname": "template",
"fieldtype": "Link",
"label": "Template",
"options": "Email Template"
},
{
"depends_on": "eval: doc.notify_by_email",
"description": "To add dynamic subject, use jinja tags like\n\n<div><pre><code>New {{ doc.doctype }} #{{ doc.name }}</code></pre></div>",
"fieldname": "subject",
"fieldtype": "Data",
"label": "Subject"
},
{
"default": "Please find attached {{ doc.doctype }} #{{ doc.name }}",
"depends_on": "eval: doc.notify_by_email",
"fieldname": "message",
"fieldtype": "Text",
"label": "Message"
},
{
"depends_on": "eval: doc.notify_by_email && doc.reference_doctype && doc.reference_document",
"fieldname": "preview_message",
"fieldtype": "Button",
"label": "Preview Message"
},
{
"depends_on": "notify_by_email",
"fieldname": "print_format",
"fieldtype": "Link",
"label": "Print Format",
"options": "Print Format"
},
{
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
"in_list_view": 1,
"label": "Status",
"options": "\nActive\nDisabled\nCompleted",
"read_only": 1
},
{
"fieldname": "section_break_3",
"fieldtype": "Section Break"
},
{
"default": "0",
"depends_on": "eval:doc.frequency === 'Monthly'",
"fieldname": "repeat_on_last_day",
"fieldtype": "Check",
"label": "Repeat on Last Day of the Month"
}
],
"modified": "2019-07-17 11:30:51.412317",
"modified_by": "Administrator",
"module": "Automation",
"name": "Auto Repeat",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
}
],
"search_fields": "reference_document",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "reference_document",
"track_changes": 1
}

View file

@ -0,0 +1,374 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.desk.form import assign_to
from frappe.utils.jinja import validate_template
from dateutil.relativedelta import relativedelta
from frappe.utils.user import get_system_managers
from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day
from frappe.model.document import Document
from frappe.core.doctype.communication.email import make
from frappe.utils.background_jobs import get_jobs
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
class AutoRepeat(Document):
def validate(self):
self.update_status()
self.validate_reference_doctype()
self.validate_dates()
self.validate_email_id()
self.set_dates()
self.update_auto_repeat_id()
self.unlink_if_applicable()
validate_template(self.subject or "")
validate_template(self.message or "")
def before_insert(self):
if not frappe.flags.in_test:
start_date = self.start_date
today_date = today()
if start_date <= today_date:
start_date = today_date
def after_save(self):
frappe.get_doc(self.reference_doctype, self.reference_document).notify_update()
def on_trash(self):
frappe.db.set_value(self.reference_doctype, self.reference_document, {
'auto_repeat': self.name
}, 'auto_repeat', '')
def set_dates(self):
if self.disabled:
self.next_schedule_date = None
else:
self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, self.end_date)
def unlink_if_applicable(self):
if self.status == 'Completed' or self.disabled:
frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '')
def validate_reference_doctype(self):
if not frappe.flags.in_test:
if not frappe.get_meta(self.reference_doctype).allow_auto_repeat:
frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype))
def validate_dates(self):
self.validate_from_to_dates('start_date', 'end_date')
if self.end_date == self.start_date:
frappe.throw(_('{0} should not be same as {1}').format(frappe.bold('End Date'), frappe.bold('Start Date')))
def validate_email_id(self):
if self.notify_by_email:
if self.recipients:
email_list = split_emails(self.recipients.replace("\n", ""))
from frappe.utils import validate_email_address
for email in email_list:
if not validate_email_address(email):
frappe.throw(_("{0} is an invalid email address in 'Recipients'").format(email))
else:
frappe.throw(_("'Recipients' not specified"))
def update_auto_repeat_id(self):
#check if document is already on auto repeat
auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat")
if auto_repeat and auto_repeat != self.name:
frappe.throw(_("The {0} is already on auto repeat {1}").format(self.reference_document, auto_repeat))
else:
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name)
def update_status(self):
if self.disabled:
self.status = "Disabled"
elif self.is_completed():
self.status = "Completed"
else:
self.status = "Active"
def is_completed(self):
return self.end_date and getdate(self.end_date) < getdate(today())
def get_auto_repeat_schedule(self):
schedule_details = []
start_date = getdate(self.start_date)
end_date = getdate(self.end_date)
today = frappe.utils.datetime.date.today()
if start_date < today:
start_date = today
if not self.end_date:
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
row = {
"reference_document": self.reference_document,
"frequency": self.frequency,
"next_scheduled_date": start_date
}
schedule_details.append(row)
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
if self.end_date:
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
while (getdate(start_date) < getdate(end_date)):
row = {
"reference_document" : self.reference_document,
"frequency" : self.frequency,
"next_scheduled_date" : start_date
}
schedule_details.append(row)
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, end_date)
return schedule_details
def create_documents(self):
try:
new_doc = self.make_new_document()
if self.notify_by_email and self.recipients:
self.send_notification(new_doc)
except Exception:
error_log = frappe.log_error(frappe.get_traceback(), _("Auto Repeat Document Creation Failure"))
self.disable_auto_repeat()
if self.reference_document and not frappe.flags.in_test:
self.notify_error_to_user(error_log)
def make_new_document(self):
reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document)
new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False)
self.update_doc(new_doc, reference_doc)
new_doc.insert(ignore_permissions = True)
return new_doc
def update_doc(self, new_doc, reference_doc):
new_doc.docstatus = 0
if new_doc.meta.get_field('set_posting_time'):
new_doc.set('set_posting_time', 1)
if new_doc.meta.get_field('auto_repeat'):
new_doc.set('auto_repeat', self.name)
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'remarks', 'owner']:
if new_doc.meta.get_field(fieldname):
new_doc.set(fieldname, reference_doc.get(fieldname))
for data in new_doc.meta.fields:
if data.fieldtype == 'Date' and data.reqd:
new_doc.set(data.fieldname, self.next_schedule_date)
self.set_auto_repeat_period(new_doc)
auto_repeat_doc = frappe.get_doc('Auto Repeat', self.name)
#for any action that needs to take place after the recurring document creation
#on recurring method of that doctype is triggered
new_doc.run_method('on_recurring', reference_doc = reference_doc, auto_repeat_doc = auto_repeat_doc)
def set_auto_repeat_period(self, new_doc):
mcount = month_map.get(self.frequency)
if mcount and new_doc.meta.get_field('from_date') and new_doc.meta.get_field('to_date'):
last_ref_doc = frappe.db.get_all(doctype = self.reference_doctype,
fields = ['name', 'from_date', 'to_date'],
filters = [
['auto_repeat', '=', self.name],
['docstatus', '<', 2],
],
order_by = 'creation desc',
limit = 1)
if not last_ref_doc:
return
from_date = get_next_date(last_ref_doc[0].from_date, mcount)
if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and \
(cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date)):
to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount))
else:
to_date = get_next_date(last_ref_doc[0].to_date, mcount)
new_doc.set('from_date', from_date)
new_doc.set('to_date', to_date)
def send_notification(self, new_doc):
"""Notify concerned people about recurring document generation"""
subject = self.subject or ''
message = self.message or ''
if not self.subject:
subject = _("New {0}: {1}").format(new_doc.doctype, new_doc.name)
elif "{" in self.subject:
subject = frappe.render_template(self.subject, {'doc': new_doc})
if not self.message:
message = _("Please find attached {0}: {1}").format(new_doc.doctype, new_doc.name)
elif "{" in self.message:
message = frappe.render_template(self.message, {'doc': new_doc})
print_format = self.print_format or 'Standard'
attachments = [frappe.attach_print(new_doc.doctype, new_doc.name,
file_name=new_doc.name, print_format=print_format)]
make(doctype=new_doc.doctype, name=new_doc.name, recipients=self.recipients,
subject=subject, content=message, attachments=attachments, send_email=1)
def fetch_linked_contacts(self):
if self.reference_doctype and self.reference_document:
res = frappe.db.get_all('Contact',
fields=['email_id'],
filters=[
['Dynamic Link', 'link_doctype', '=', self.reference_doctype],
['Dynamic Link', 'link_name', '=', self.reference_document]
])
email_ids = list(set([d.email_id for d in res]))
if not email_ids:
frappe.msgprint(_('No contacts linked to document'), alert=True)
else:
self.recipients = ', '.join(email_ids)
def disable_auto_repeat(self):
frappe.db.set_value('Auto Repeat', self.name, 'disabled', 1)
def notify_error_to_user(self, error_log):
recipients = get_system_managers(only_name=True) + self.owner
subject = _("Auto Repeat Document Creation Failed")
form_link = frappe.utils.get_link_to_form(self.reference_doctype, self.reference_document)
auto_repeat_failed_for = _('Auto Repeat failed for {0}').format(form_link)
error_log_link =frappe.utils.get_link_to_form(error_log.reference_doctype, error_log.reference_document)
error_log_message = _('Check the Error Log for more information: {0}').format(error_log_link)
frappe.sendmail(
recipients=recipients,
subject=subject,
template="auto_repeat_fail",
args={
'auto_repeat_failed_for': auto_repeat_failed_for,
'error_log_message': error_log_message
},
header=[subject, 'red']
)
def get_next_schedule_date(start_date, frequency, repeat_on_day, repeat_on_last_day = False, end_date = None):
month_count = month_map.get(frequency)
if month_count and repeat_on_last_day:
next_date = get_next_date(start_date, month_count, 31)
elif month_count and repeat_on_day:
next_date = get_next_date(start_date, month_count, repeat_on_day)
elif month_count:
next_date = get_next_date(start_date, month_count)
else:
days = 7 if frequency == 'Weekly' else 1
next_date = add_days(start_date, days)
return next_date
def get_next_date(dt, mcount, day=None):
dt = getdate(dt)
dt += relativedelta(months=mcount, day=day)
return dt
#called through hooks
def make_auto_repeat_entry():
enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries'
jobs = get_jobs()
if not jobs or enqueued_method not in jobs[frappe.local.site]:
date = getdate(today())
data = get_auto_repeat_entries(date)
frappe.enqueue(enqueued_method, data=data)
def create_repeated_entries(data):
for d in data:
doc = frappe.get_doc('Auto Repeat', d.name)
current_date = getdate(today())
schedule_date = getdate(doc.next_schedule_date)
while schedule_date <= current_date and not doc.disabled:
doc.create_documents()
schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date)
if schedule_date and not doc.disabled:
frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date)
def get_auto_repeat_entries(date=None):
if not date:
date = getdate(today())
return frappe.db.get_all('Auto Repeat', filters=[
['next_schedule_date', '<=', date],
['status', '=', 'Active']
])
#called through hooks
def set_auto_repeat_as_completed():
auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']})
for entry in auto_repeat:
doc = frappe.get_doc("Auto Repeat", entry.name)
if doc.is_completed():
doc.status = 'Completed'
doc.save()
@frappe.whitelist()
def make_auto_repeat(doctype, docname, frequency, start_date, end_date = None):
doc = frappe.new_doc('Auto Repeat')
doc.reference_doctype = doctype
doc.reference_document = docname
doc.frequency = frequency
doc.start_date = start_date
if end_date:
doc.end_date = end_date
doc.save()
return doc
#method for reference_doctype filter
def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters):
res = frappe.db.get_all('Property Setter', {
'property': 'allow_auto_repeat',
'value': '1',
}, ['doc_type'])
docs = [r.doc_type for r in res]
res = frappe.db.get_all('DocType', {
'allow_auto_repeat': 1,
}, ['name'])
docs += [r.name for r in res]
docs = set(list(docs))
return [[d] for d in docs]
@frappe.whitelist()
def update_reference(docname, reference):
result = ""
try:
frappe.db.set_value("Auto Repeat", docname, "reference_document", reference)
result = "success"
except Exception as e:
result = "error"
raise e
return result
@frappe.whitelist()
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
doc = frappe.get_doc(reference_dt, reference_doc)
subject_preview = _("Please add a subject to your email")
msg_preview = frappe.render_template(message, {'doc': doc})
if subject:
subject_preview = frappe.render_template(subject, {'doc': doc})
return {'message': msg_preview, 'subject': subject_preview}

View file

@ -0,0 +1,11 @@
frappe.listview_settings['Auto Repeat'] = {
add_fields: ["next_schedule_date"],
get_indicator: function(doc) {
var colors = {
"Active": "green",
"Disabled": "red",
"Completed": "blue",
};
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
}
};

View file

@ -11,10 +11,9 @@
{% for(var i=0; i < schedule_details.length; i++) { %}
<tr>
<td>{{ schedule_details[i].reference_document }}</td>
<td> {{ schedule_details[i].frequency }} </td>
<td> {{ schedule_details[i].next_scheduled_date }} </td>
<td> {{ __(schedule_details[i].frequency) }} </td>
<td> {{ frappe.datetime.str_to_user(schedule_details[i].next_scheduled_date) }} </td>
</tr>
{% } %}
</tbody>
</table>

View file

@ -7,20 +7,19 @@ import unittest
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.desk.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, disable_auto_repeat
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries
from frappe.utils import today, add_days, getdate, add_months
def add_custom_fields():
df = dict(
fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender',
options='Auto Repeat')
options='Auto Repeat', hidden=1, print_hide=1, read_only=1)
create_custom_field('ToDo', df)
class TestAutoRepeat(unittest.TestCase):
def setUp(self):
if not frappe.db.sql("SELECT `name` FROM `tabCustom Field` WHERE `name`='auto_repeat'"):
if not frappe.db.sql("SELECT `fieldname` FROM `tabCustom Field` WHERE `fieldname`='auto_repeat' and `dt`=%s", "Todo"):
add_custom_fields()
def test_daily_auto_repeat(self):
@ -29,8 +28,8 @@ class TestAutoRepeat(unittest.TestCase):
doc = make_auto_repeat(reference_document=todo.name)
self.assertEqual(doc.next_schedule_date, today())
for data in get_auto_repeat_entries(today()):
create_repeated_entries(data)
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
frappe.db.commit()
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
@ -51,8 +50,11 @@ class TestAutoRepeat(unittest.TestCase):
dict(doctype='ToDo', description='test recurring todo', assigned_by='Administrator')).insert()
self.monthly_auto_repeat('ToDo', todo.name, start_date, end_date)
#test without end_date
todo = frappe.get_doc(dict(doctype='ToDo', description='test recurring todo without end_date', assigned_by='Administrator')).insert()
self.monthly_auto_repeat('ToDo', todo.name, start_date)
def monthly_auto_repeat(self, doctype, docname, start_date, end_date):
def monthly_auto_repeat(self, doctype, docname, start_date, end_date = None):
def get_months(start, end):
diff = (12 * end.year + end.month) - (12 * start.year + start.month)
return diff + 1
@ -61,10 +63,10 @@ class TestAutoRepeat(unittest.TestCase):
reference_doctype=doctype, frequency='Monthly', reference_document=docname, start_date=start_date,
end_date=end_date)
disable_auto_repeat(doc)
doc.disable_auto_repeat()
for data in get_auto_repeat_entries(today()):
create_repeated_entries(data)
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name})
self.assertEqual(len(docnames), 1)
@ -72,8 +74,8 @@ class TestAutoRepeat(unittest.TestCase):
doc.db_set('disabled', 0)
months = get_months(getdate(start_date), getdate(today()))
for data in get_auto_repeat_entries(today()):
create_repeated_entries(data)
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name})
self.assertEqual(len(docnames), months)
@ -84,8 +86,8 @@ class TestAutoRepeat(unittest.TestCase):
doc = make_auto_repeat(reference_document=todo.name, notify=1, recipients="test@domain.com", subject="New ToDo",
message="A new ToDo has just been created for you")
for data in get_auto_repeat_entries(today()):
create_repeated_entries(data)
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
frappe.db.commit()
new_todo = frappe.db.get_value('ToDo',
@ -100,18 +102,14 @@ def make_auto_repeat(**args):
doc = frappe.get_doc({
'doctype': 'Auto Repeat',
'reference_doctype': args.reference_doctype or 'ToDo',
'reference_document': args.reference_document or frappe.db.get_value('ToDo', {'docstatus': 1}, 'name'),
'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'),
'frequency': args.frequency or 'Daily',
'start_date': args.start_date or add_days(today(), -1),
'end_date': args.end_date or add_days(today(), 2),
'submit_on_creation': args.submit_on_creation or 0,
'end_date': args.end_date or "",
'notify_by_email': args.notify or 0,
'recipients': args.recipients or "",
'subject': args.subject or "",
'message': args.message or ""
}).insert(ignore_permissions=True)
if not args.do_not_submit:
doc.submit()
return doc

View file

@ -92,11 +92,6 @@ def get_data():
"name": "Google Settings",
"description": _("Google API Settings."),
},
{
"type": "doctype",
"name": "Google Maps Settings",
"description": _("Google Maps integration"),
},
{
"type": "doctype",
"name": "GCalendar Settings",

View file

@ -169,11 +169,27 @@ def get_data():
"name": "Workflow Action",
"description": _("Actions for workflow (e.g. Approve, Cancel).")
},
]
},
{
"label": _("Automation"),
"icon": "fa fa-random",
"items": [
{
"type": "doctype",
"name": "Assignment Rule",
"description": _("Set up rules for user assignments.")
}
},
{
"type": "doctype",
"name": "Milestone",
"description": _("Tracks milestones on the lifecycle of a document if it undergoes multiple stages.")
},
{
"type": "doctype",
"name": "Auto Repeat",
"description": _("Automatically generates recurring documents.")
},
]
},
]

View file

@ -161,3 +161,16 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters):
'link_name': link_name,
'link_doctype': link_doctype
})
def get_contact_with_phone_number(number):
if not number: return
contacts = frappe.get_all('Contact', or_filters={
'phone': ['like', '%{}'.format(number)],
'mobile_no': ['like', '%{}'.format(number)]
}, limit=1)
contact = contacts[0].name if contacts else None
return contact

View file

@ -529,7 +529,7 @@ def update_mins_to_first_communication(parent, communication):
if frappe.db.get_all('User', filters={'email': communication.sender,
'user_type': 'System User', 'enabled': 1}, limit=1):
first_responded_on = communication.creation
if parent.meta.has_field('first_responded_on'):
if parent.meta.has_field('first_responded_on') and communication.sent_or_received == "Sent":
parent.db_set('first_responded_on', first_responded_on)
parent.db_set('mins_to_first_response', round(time_diff_in_seconds(first_responded_on, parent.creation) / 60), 2)

View file

@ -22,9 +22,15 @@ frappe.ui.form.on('DocType', {
}
if (!frm.is_new() && !frm.doc.istable) {
frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => {
frappe.set_route('List', frm.doc.name, 'List');
});
if (frm.doc.issingle) {
frm.add_custom_button(__('Go to {0}', [frm.doc.name]), () => {
frappe.set_route('Form', frm.doc.name);
});
} else {
frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => {
frappe.set_route('List', frm.doc.name, 'List');
});
}
}
if(!frappe.boot.developer_mode && !frm.doc.custom) {

View file

@ -37,6 +37,7 @@
"allow_rename",
"allow_import",
"allow_events_in_timeline",
"allow_auto_repeat",
"view_settings",
"title_field",
"search_fields",
@ -81,6 +82,7 @@
"search_index": 1
},
{
"default": "0",
"depends_on": "eval:!doc.istable",
"description": "Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended.",
"fieldname": "is_submittable",
@ -88,6 +90,7 @@
"label": "Is Submittable"
},
{
"default": "0",
"description": "Child Tables are shown as a Grid in other DocTypes",
"fieldname": "istable",
"fieldtype": "Check",
@ -97,6 +100,7 @@
"oldfieldtype": "Check"
},
{
"default": "0",
"depends_on": "eval:!doc.istable",
"description": "Single Types have only one record no tables associated. Values are stored in tabSingles",
"fieldname": "issingle",
@ -135,6 +139,7 @@
"label": "Track Changes"
},
{
"default": "0",
"depends_on": "eval:!doc.istable",
"description": "If enabled, the document is marked as seen, the first time a user opens it",
"fieldname": "track_seen",
@ -150,11 +155,13 @@
"label": "Track Views"
},
{
"default": "0",
"fieldname": "custom",
"fieldtype": "Check",
"label": "Custom?"
},
{
"default": "0",
"fieldname": "beta",
"fieldtype": "Check",
"label": "Beta"
@ -236,6 +243,7 @@
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "hide_toolbar",
"fieldtype": "Check",
"label": "Hide Sidebar and Menu",
@ -243,6 +251,7 @@
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "allow_copy",
"fieldtype": "Check",
"label": "Hide Copy",
@ -250,6 +259,7 @@
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "allow_rename",
"fieldtype": "Check",
"label": "Allow Rename",
@ -257,15 +267,23 @@
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "allow_import",
"fieldtype": "Check",
"label": "Allow Import (via Data Import Tool)"
},
{
"default": "0",
"fieldname": "allow_events_in_timeline",
"fieldtype": "Check",
"label": "Allow events in timeline"
},
{
"default": "0",
"fieldname": "allow_auto_repeat",
"fieldtype": "Check",
"label": "Allow Auto Repeat"
},
{
"collapsible": 1,
"fieldname": "view_settings",
@ -329,6 +347,13 @@
"label": "Color"
},
{
"default": "0",
"fieldname": "show_preview_popup",
"fieldtype": "Check",
"label": "Show Preview Popup"
},
{
"default": "0",
"fieldname": "show_name_in_global_search",
"fieldtype": "Check",
"label": "Make \"name\" searchable in Global Search"
@ -354,6 +379,7 @@
"options": "Domain"
},
{
"default": "0",
"fieldname": "read_only",
"fieldtype": "Check",
"label": "User Cannot Search",
@ -361,6 +387,7 @@
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "in_create",
"fieldtype": "Check",
"label": "User Cannot Create",
@ -411,17 +438,11 @@
"fieldtype": "Select",
"label": "Database Engine",
"options": "InnoDB\nMyISAM"
},
{
"default": "0",
"fieldname": "show_preview_popup",
"fieldtype": "Check",
"label": "Show Preview Popup"
}
],
"icon": "fa fa-bolt",
"idx": 6,
"modified": "2019-05-16 14:58:33.405381",
"modified": "2019-07-04 23:23:17.174960",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
@ -454,4 +475,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View file

@ -13,6 +13,7 @@ from frappe.utils import now, cint
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.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.desk.notifications import delete_notification_count_for
from frappe.modules import make_boilerplate, get_doc_path
from frappe.database.schema import validate_column_name, validate_column_length
@ -47,7 +48,8 @@ class DocType(Document):
- Validate series
- Check fieldnames (duplication etc)
- Clear permission table for child tables
- Add `amended_from` and `amended_by` if Amendable"""
- Add `amended_from` and `amended_by` if Amendable
- Add custom field `auto_repeat` if Repeatable"""
self.check_developer_mode()
@ -76,6 +78,7 @@ class DocType(Document):
validate_permissions(self)
self.make_amendable()
self.make_repeatable()
self.validate_website()
if not self.is_new():
@ -526,6 +529,14 @@ class DocType(Document):
"no_copy": 1
})
def make_repeatable(self):
"""If allow_auto_repeat is set, add auto_repeat custom field."""
if self.allow_auto_repeat:
if not frappe.db.exists('Custom Field', {'fieldname': 'auto_repeat', 'dt': self.name}):
insert_after = self.fields[len(self.fields) - 1].fieldname
df = dict(fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', options='Auto Repeat', insert_after=insert_after, read_only=1, no_copy=1, print_hide=1)
create_custom_field(self.name, df)
def get_max_idx(self):
"""Returns the highest `idx`"""
max_idx = frappe.db.sql("""select max(idx) from `tabDocField` where parent = %s""",

View file

@ -1,173 +1,72 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 1,
"autoname": "field:language_code",
"beta": 0,
"creation": "2014-08-22 16:12:17.249590",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "language_code",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Language Code",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "language_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Language Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "flag",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Flag",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "based_on",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Based On",
"length": 0,
"no_copy": 0,
"options": "Language",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-globe",
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-12-29 14:40:33.210645",
"modified_by": "Administrator",
"module": "Core",
"name": "Language",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "language_name",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "language_name",
"track_changes": 1,
"track_seen": 0
}
"allow_rename": 1,
"autoname": "field:language_code",
"creation": "2014-08-22 16:12:17.249590",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"language_code",
"language_name",
"flag",
"based_on"
],
"fields": [
{
"fieldname": "language_code",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Language Code",
"reqd": 1,
"unique": 1
},
{
"fieldname": "language_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Language Name",
"reqd": 1
},
{
"fieldname": "flag",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Flag"
},
{
"fieldname": "based_on",
"fieldtype": "Link",
"label": "Based On",
"options": "Language"
}
],
"icon": "fa fa-globe",
"modified": "2019-07-19 16:32:12.652550",
"modified_by": "Administrator",
"module": "Core",
"name": "Language",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"read": 1,
"role": "System Manager",
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Guest",
"share": 1
}
],
"search_fields": "language_name",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "language_name",
"track_changes": 1
}

View file

@ -12,6 +12,7 @@ from frappe.modules.export_file import export_to_files
from frappe.modules import make_boilerplate
from frappe.core.doctype.page.page import delete_custom_role
from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles
from frappe.desk.reportview import append_totals_row
from six import iteritems
@ -76,11 +77,6 @@ class Report(Document):
if not self.json:
self.json = '{}'
if self.json:
data = json.loads(self.json)
data["add_total_row"] = self.add_total_row
self.json = json.dumps(data)
def export_doc(self):
if frappe.flags.in_import:
return
@ -178,6 +174,9 @@ class Report(Document):
out = out + [list(d) for d in result]
if params.get('add_totals_row'):
out = append_totals_row(out)
if as_dict:
data = []
for row in out:

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,7 @@
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"doc_type",
"properties",
@ -16,6 +17,7 @@
"quick_entry",
"track_changes",
"track_views",
"allow_auto_repeat",
"image_view",
"column_break_5",
"title_field",
@ -59,17 +61,20 @@
"label": "Max Attachments"
},
{
"default": "0",
"fieldname": "allow_copy",
"fieldtype": "Check",
"label": "Hide Copy"
},
{
"default": "0",
"fieldname": "istable",
"fieldtype": "Check",
"label": "Is Table",
"read_only": 1
},
{
"default": "0",
"depends_on": "istable",
"fieldname": "editable_grid",
"fieldtype": "Check",
@ -82,11 +87,13 @@
"label": "Quick Entry"
},
{
"default": "0",
"fieldname": "track_changes",
"fieldtype": "Check",
"label": "Track Changes"
},
{
"default": "0",
"depends_on": "eval: doc.image_field",
"fieldname": "image_view",
"fieldtype": "Check",
@ -150,16 +157,23 @@
"options": "Customize Form Field"
},
{
"default": "0",
"fieldname": "track_views",
"fieldtype": "Check",
"label": "Track Views"
},
{
"default": "0",
"fieldname": "allow_auto_repeat",
"fieldtype": "Check",
"label": "Allow Auto Repeat"
}
],
"hide_toolbar": 1,
"icon": "fa fa-glass",
"idx": 1,
"issingle": 1,
"modified": "2019-05-13 18:54:40.610862",
"modified": "2019-07-01 22:50:50.372465",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",
@ -177,6 +191,7 @@
],
"quick_entry": 1,
"search_fields": "doc_type",
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -13,6 +13,7 @@ from frappe.utils import cint
from frappe.model.document import Document
from frappe.model import no_value_fields, core_doctypes_list
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.model.docfield import supports_translation
doctype_properties = {
@ -29,6 +30,7 @@ doctype_properties = {
'max_attachments': 'Int',
'track_changes': 'Check',
'track_views': 'Check',
'allow_auto_repeat': 'Check'
}
docfield_properties = {
@ -65,6 +67,7 @@ docfield_properties = {
'columns': 'Int',
'remember_last_selected_value': 'Check',
'allow_bulk_edit': 'Check',
'auto_repeat': 'Link'
}
allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'),
@ -108,6 +111,13 @@ class CustomizeForm(Document):
translation = self.get_name_translation()
self.label = translation.target_name if translation else ''
#If allow_auto_repeat is set, add auto_repeat custom field.
if self.allow_auto_repeat:
if not frappe.db.exists('Custom Field', {'fieldname': 'auto_repeat', 'dt': self.doc_type}):
insert_after = self.fields[len(self.fields) - 1].fieldname
df = dict(fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', options='Auto Repeat', insert_after=insert_after, read_only=1, no_copy=1, print_hide=1)
create_custom_field(self.doc_type, df)
# NOTE doc is sent to clientside by run_method
def get_name_translation(self):

View file

@ -553,6 +553,10 @@ class Database(object):
val = val[0][0] if val else None
df = frappe.get_meta(doctype).get_field(fieldname)
if not df:
frappe.throw(_('Invalid field name: {0}').format(frappe.bold(fieldname)), self.InvalidColumnName)
if df.fieldtype in frappe.model.numeric_fieldtypes:
val = cint(val)

View file

@ -48,7 +48,10 @@ class DbManager:
if not host:
host = self.get_current_host()
self.db.sql("GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'%s';" % (target, user, host))
if frappe.conf.get('rds_db', 0) == 1:
self.db.sql("GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EXECUTE ON `%s`.* TO '%s'@'%s';" % (target, user, host))
else:
self.db.sql("GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'%s';" % (target, user, host))
def flush_privileges(self):
self.db.sql("FLUSH PRIVILEGES")

View file

@ -111,13 +111,10 @@ class PostgresDatabase(Database):
def format_date(self, date):
if not date:
return '0001-01-01::DATE'
return '0001-01-01'
if isinstance(date, frappe.string_types):
if ':' not in date:
date = date + '::DATE'
else:
date = date.strftime('%Y-%m-%d') + '::DATE'
if not isinstance(date, frappe.string_types):
date = date.strftime('%Y-%m-%d')
return date

View file

@ -1,144 +0,0 @@
// Copyright (c) 2018, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.provide("frappe.auto_repeat");
frappe.ui.form.on('Auto Repeat', {
setup: function(frm) {
frm.fields_dict['reference_doctype'].get_query = function() {
return {
query: "frappe.desk.doctype.auto_repeat.auto_repeat.auto_repeat_doctype_query"
};
};
frm.fields_dict['reference_document'].get_query = function() {
return {
filters: {
"docstatus": 1,
"auto_repeat": ''
}
};
};
frm.fields_dict['print_format'].get_query = function() {
return {
filters: {
"doc_type": frm.doc.reference_doctype
}
};
};
},
refresh: function(frm) {
if(frm.doc.docstatus == 1) {
let label = __('View {0}', [__(frm.doc.reference_doctype)]);
frm.add_custom_button(__(label),
function() {
frappe.route_options = {
"auto_repeat": frm.doc.name,
};
frappe.set_route("List", frm.doc.reference_doctype);
}
);
if(frm.doc.status != 'Stopped') {
frm.add_custom_button(__("Stop"),
function() {
frm.events.stop_resume_auto_repeat(frm, "Stopped");
}
);
}
if(frm.doc.status == 'Stopped') {
frm.add_custom_button(__("Restart"),
function() {
frm.events.stop_resume_auto_repeat(frm, "Resumed");
}
);
}
}
frm.toggle_display('auto_repeat_schedule', !in_list(['Stopped', 'Cancelled'], frm.doc.status));
if(frm.doc.start_date && !in_list(['Stopped', 'Cancelled'], frm.doc.status)){
frappe.auto_repeat.render_schedule(frm);
}
},
stop_resume_auto_repeat: function(frm, status) {
frappe.call({
method: "frappe.desk.doctype.auto_repeat.auto_repeat.stop_resume_auto_repeat",
args: {
auto_repeat: frm.doc.name,
status: status
},
callback: function(r) {
if(r.message) {
frm.set_value("status", r.message);
frm.reload_doc();
}
}
});
},
template: function(frm) {
if (frm.doc.template) {
frappe.model.with_doc("Email Template", frm.doc.template, () => {
let email_template = frappe.get_doc("Email Template", frm.doc.template);
frm.set_value("subject", email_template.subject);
frm.set_value("message", email_template.response);
frm.refresh_field("subject");
frm.refresh_field("message");
});
}
},
get_contacts: function(frm) {
frappe.call({
method: "frappe.desk.doctype.auto_repeat.auto_repeat.get_contacts",
args: {
reference_doctype: frm.doc.reference_doctype,
reference_name: frm.doc.reference_document
},
callback: function(r) {
if(r.message) {
frm.set_value("recipients", r.message.join());
frm.refresh_field("recipients");
}
}
});
},
preview_message: function(frm) {
if (frm.doc.message) {
frappe.call({
method: "frappe.desk.doctype.auto_repeat.auto_repeat.generate_message_preview",
args: {
reference_dt: frm.doc.reference_doctype,
reference_doc: frm.doc.reference_document,
subject: frm.doc.subject,
message: frm.doc.message
},
callback: function(r) {
if(r.message) {
frappe.msgprint(r.message.message, r.message.subject)
}
}
});
} else {
frappe.msgprint(__("Please setup a message first"), __("Message not setup"))
}
}
});
frappe.auto_repeat.render_schedule = function(frm) {
frappe.call({
method: "get_auto_repeat_schedule",
doc: frm.doc
}).done((r) => {
var wrapper = $(frm.fields_dict["auto_repeat_schedule"].wrapper);
wrapper.html(frappe.render_template ("auto_repeat_schedule", {"schedule_details" : r.message || []} ));
frm.refresh_fields();
});
};

File diff suppressed because it is too large Load diff

View file

@ -1,418 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import calendar
from frappe import _
from frappe.desk.form import assign_to
from frappe.utils.jinja import validate_template
from dateutil.relativedelta import relativedelta
from frappe.utils.user import get_system_managers
from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day
from frappe.model.document import Document
from frappe.core.doctype.communication.email import make
from frappe.utils.background_jobs import get_jobs
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
class AutoRepeat(Document):
def validate(self):
self.update_status()
self.validate_reference_doctype()
self.validate_dates()
self.validate_next_schedule_date()
self.validate_email_id()
self.link_party()
validate_template(self.subject or "")
validate_template(self.message or "")
def before_submit(self):
start_date_copy = self.start_date
today_copy = add_days(today(), -1)
if start_date_copy <= today_copy:
start_date_copy = today_copy
if not self.next_schedule_date:
self.next_schedule_date = get_next_schedule_date(
start_date_copy, self.frequency, self.repeat_on_day)
def on_submit(self):
self.update_auto_repeat_id()
def on_cancel(self):
self.update_status()
def on_update_after_submit(self):
self.validate_dates()
self.set_next_schedule_date()
def before_cancel(self):
self.unlink_auto_repeat_id()
self.next_schedule_date = None
def unlink_auto_repeat_id(self):
frappe.db.sql(
"update `tab{0}` set auto_repeat = null where auto_repeat=%s".format(self.reference_doctype), self.name)
def validate_reference_doctype(self):
if not frappe.get_meta(self.reference_doctype).has_field('auto_repeat'):
frappe.throw(_("Add custom field Auto Repeat in the doctype {0}").format(self.reference_doctype))
def validate_dates(self):
if self.end_date and getdate(self.start_date) > getdate(self.end_date):
frappe.throw(_("End date must be greater than start date"))
def validate_next_schedule_date(self):
if self.repeat_on_day and self.next_schedule_date:
next_date = getdate(self.next_schedule_date)
if next_date.day != self.repeat_on_day:
# if the repeat day is the last day of the month (31)
# and the current month does not have as many days,
# then the last day of the current month is a valid date
lastday = calendar.monthrange(next_date.year, next_date.month)[1]
if self.repeat_on_day < lastday:
# the specified day of the month is not same as the day specified
# or the last day of the month
frappe.throw(_("Next Date's day and Repeat on Day of Month must be equal"))
def validate_email_id(self):
if self.notify_by_email:
if self.recipients:
email_list = split_emails(self.recipients.replace("\n", ""))
from frappe.utils import validate_email_address
for email in email_list:
if not validate_email_address(email):
frappe.throw(_("{0} is an invalid email address in 'Recipients'").format(email))
else:
frappe.throw(_("'Recipients' not specified"))
def set_next_schedule_date(self):
if self.repeat_on_day:
self.next_schedule_date = get_next_date(self.next_schedule_date, 0, self.repeat_on_day)
def update_auto_repeat_id(self):
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name)
def update_status(self, status=None):
self.status = {
'0': 'Draft',
'1': 'Submitted',
'2': 'Cancelled'
}[cstr(self.docstatus or 0)]
if status and status != 'Resumed':
self.status = status
if self.docstatus == 2:
self.db_set("status", self.status)
def get_auto_repeat_schedule(self):
schedule_details = []
start_date_copy = getdate(self.start_date)
end_date_copy = getdate(self.end_date)
today_copy = frappe.utils.datetime.date.today()
if start_date_copy < today_copy:
start_date_copy = today_copy
if not self.end_date:
days = 60 if self.frequency in ['Daily', 'Weekly'] else 365
end_date_copy = add_days(today_copy, days)
start_date_copy = get_next_schedule_date(start_date_copy, self.frequency, self.repeat_on_day)
while (getdate(start_date_copy) < getdate(end_date_copy)):
row = {
"reference_document" : self.reference_document,
"frequency" : self.frequency,
"next_scheduled_date" : start_date_copy
}
schedule_details.append(row)
start_date_copy = get_next_schedule_date(start_date_copy, self.frequency, self.repeat_on_day)
return schedule_details
def link_party(self):
reference = frappe.get_meta(self.reference_doctype)
for field in reference.fields:
if field.options in ['Customer', 'Supplier', 'Employee']:
self.reference_party_doctype = field.options
self.reference_party = frappe.db.get_value(self.reference_doctype, self.reference_document, field.fieldname)
break
def get_next_schedule_date(start_date, frequency, repeat_on_day):
mcount = month_map.get(frequency)
if mcount:
next_date = get_next_date(start_date, mcount, repeat_on_day)
else:
days = 7 if frequency == 'Weekly' else 1
next_date = add_days(start_date, days)
return next_date
def make_auto_repeat_entry(date=None):
enqueued_method = 'frappe.desk.doctype.auto_repeat.auto_repeat.create_repeated_entries'
jobs = get_jobs()
if not jobs or enqueued_method not in jobs[frappe.local.site]:
date = date or today()
for data in get_auto_repeat_entries(date):
frappe.enqueue(enqueued_method, data=data)
def create_repeated_entries(data):
schedule_date = getdate(data.next_schedule_date)
while schedule_date <= getdate(today()) and not frappe.db.get_value('Auto Repeat', data.name, 'disabled'):
create_documents(data, schedule_date)
schedule_date = get_next_schedule_date(schedule_date, data.frequency, data.repeat_on_day)
if schedule_date and not frappe.db.get_value('Auto Repeat', data.name, 'disabled'):
frappe.db.set_value('Auto Repeat', data.name, 'next_schedule_date', schedule_date)
frappe.db.commit()
def get_auto_repeat_entries(date):
return frappe.db.sql(""" select * from `tabAuto Repeat`
where docstatus = 1 and next_schedule_date <=%s
and reference_document is not null and reference_document != ''
and next_schedule_date <= ifnull(end_date, '2199-12-31')
and disabled = 0 and status != 'Stopped' """, (date), as_dict=1)
def create_documents(data, schedule_date):
try:
doc = make_new_document(data, schedule_date)
if data.notify_by_email and data.recipients:
print_format = data.print_format or "Standard"
send_notification(doc, data, print_format=print_format)
frappe.db.commit()
except Exception:
frappe.db.rollback()
frappe.db.begin()
frappe.log_error(frappe.get_traceback(), _("Recurring document creation failure"))
disable_auto_repeat(data)
frappe.db.commit()
if data.reference_document and not frappe.flags.in_test:
notify_error_to_user(data)
def disable_auto_repeat(data):
auto_repeat = frappe.get_doc('Auto Repeat', data.name)
auto_repeat.db_set('disabled', 1)
def notify_error_to_user(data):
party = ''
party_type = ''
if data.reference_doctype in ['Sales Order', 'Sales Invoice', 'Delivery Note']:
party_type = 'customer'
elif data.reference_doctype in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']:
party_type = 'supplier'
if party_type:
party = frappe.db.get_value(data.reference_doctype, data.reference_document, party_type)
notify_errors(data.reference_document, data.reference_doctype, party, data.owner, data.name)
def make_new_document(args, schedule_date):
doc = frappe.get_doc(args.reference_doctype, args.reference_document)
new_doc = frappe.copy_doc(doc, ignore_no_copy=False)
update_doc(new_doc, doc, args, schedule_date)
new_doc.insert(ignore_permissions=True)
if args.submit_on_creation:
new_doc.submit()
return new_doc
def update_doc(new_document, reference_doc, args, schedule_date):
new_document.docstatus = 0
if new_document.meta.get_field('set_posting_time'):
new_document.set('set_posting_time', 1)
mcount = month_map.get(args.frequency)
if new_document.meta.get_field('auto_repeat'):
new_document.set('auto_repeat', args.name)
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time',
'select_print_heading', 'remarks', 'owner']:
if new_document.meta.get_field(fieldname):
new_document.set(fieldname, reference_doc.get(fieldname))
# copy item fields
if new_document.meta.get_field('items'):
for i, item in enumerate(new_document.items):
for fieldname in ("page_break",):
item.set(fieldname, reference_doc.items[i].get(fieldname))
for data in new_document.meta.fields:
if data.fieldtype == 'Date' and data.reqd:
new_document.set(data.fieldname, schedule_date)
set_auto_repeat_period(args, mcount, new_document)
new_document.run_method("on_recurring", reference_doc=reference_doc, auto_repeat_doc=args)
def set_auto_repeat_period(args, mcount, new_document):
if mcount and new_document.meta.get_field('from_date') and new_document.meta.get_field('to_date'):
last_ref_doc = frappe.db.sql("""
select name, from_date, to_date
from `tab{0}`
where auto_repeat=%s and docstatus < 2
order by creation desc
limit 1
""".format(args.reference_doctype), args.name, as_dict=1)
if not last_ref_doc:
return
from_date = get_next_date(last_ref_doc[0].from_date, mcount)
if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and \
(cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date)):
to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount))
else:
to_date = get_next_date(last_ref_doc[0].to_date, mcount)
new_document.set('from_date', from_date)
new_document.set('to_date', to_date)
def get_next_date(dt, mcount, day=None):
dt = getdate(dt)
dt += relativedelta(months=mcount, day=day)
return dt
def send_notification(new_rv, auto_repeat_doc, print_format='Standard'):
"""Notify concerned persons about recurring document generation"""
print_format = print_format
subject = auto_repeat_doc.subject or ''
message = auto_repeat_doc.message or ''
if not auto_repeat_doc.subject:
subject = _("New {0}: #{1}").format(new_rv.doctype, new_rv.name)
elif "{" in auto_repeat_doc.subject:
subject = frappe.render_template(auto_repeat_doc.subject, {'doc': new_rv})
if not auto_repeat_doc.message:
message = _("Please find attached {0} #{1}").format(new_rv.doctype, new_rv.name)
elif "{" in auto_repeat_doc.message:
message = frappe.render_template(auto_repeat_doc.message, {'doc': new_rv})
attachments = [frappe.attach_print(new_rv.doctype, new_rv.name,
file_name=new_rv.name, print_format=print_format)]
make(doctype=new_rv.doctype, name=new_rv.name, recipients=auto_repeat_doc.recipients,
subject=subject, content=message, attachments=attachments, send_email=1)
def notify_errors(doc, doctype, party, owner, name):
recipients = get_system_managers(only_name=True)
frappe.sendmail(recipients + [frappe.db.get_value("User", owner, "email")],
subject=_("[Urgent] Error while creating recurring %s for %s" % (doctype, doc)),
message=frappe.get_template("templates/emails/recurring_document_failed.html").render({
"type": _(doctype),
"name": doc,
"party": party or "",
"auto_repeat": name
}))
try:
assign_task_to_owner(name, _("Recurring Documents Failed"), recipients)
except Exception:
frappe.log_error(frappe.get_traceback(), _("Recurring Documents Failed"))
def assign_task_to_owner(name, msg, users):
for d in users:
args = {
'doctype': 'Auto Repeat',
'assign_to': d,
'name': name,
'description': msg,
'priority': 'High'
}
assign_to.add(args)
@frappe.whitelist()
def make_auto_repeat(doctype, docname):
doc = frappe.new_doc('Auto Repeat')
reference_doc = frappe.get_doc(doctype, docname)
doc.reference_doctype = doctype
doc.reference_document = docname
doc.start_date = reference_doc.get('posting_date') or reference_doc.get('transaction_date')
return doc
@frappe.whitelist()
def stop_resume_auto_repeat(auto_repeat, status):
doc = frappe.get_doc('Auto Repeat', auto_repeat)
frappe.msgprint(_("Auto Repeat has been {0}").format(status))
if status == 'Resumed':
doc.next_schedule_date = get_next_schedule_date(today(),
doc.frequency, doc.repeat_on_day)
doc.update_status(status)
doc.save()
return doc.status
def auto_repeat_doctype_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql("""select parent from `tabDocField`
where fieldname = 'auto_repeat'
and parent like %(txt)s
order by
if(locate(%(_txt)s, parent), locate(%(_txt)s, parent), 99999),
parent
limit %(start)s, %(page_len)s""".format(**{
'key': searchfield,
}), {
'txt': "%%%s%%" % txt,
'_txt': txt.replace("%", ""),
'start': start,
'page_len': page_len
})
@frappe.whitelist()
def get_contacts(reference_doctype, reference_name):
docfields = frappe.get_meta(reference_doctype).fields
contact_fields = []
for field in docfields:
if field.fieldtype == "Link" and field.options == "Contact":
contact_fields.append(field.fieldname)
if contact_fields:
contacts = []
for contact_field in contact_fields:
contacts.append(frappe.db.get_value(reference_doctype, reference_name, contact_field))
else:
return []
if contacts:
emails = []
for contact in contacts:
emails.append(frappe.db.get_value("Contact", contact, "email_id"))
return emails
else:
return []
@frappe.whitelist()
def update_reference(docname, reference):
try:
frappe.db.set_value("Auto Repeat", docname, "reference_document", reference)
return "success"
except Exception as e:
raise e
return "error"
@frappe.whitelist()
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
doc = frappe.get_doc(reference_dt, reference_doc)
subject_preview = _("Please add a subject to your email")
msg_preview = frappe.render_template(message, {'doc': doc})
if subject:
subject_preview = frappe.render_template(subject, {'doc': doc})
return {'message': msg_preview, 'subject': subject_preview}

View file

@ -1,16 +0,0 @@
frappe.listview_settings['Auto Repeat'] = {
add_fields: ["next_schedule_date"],
get_indicator: function(doc) {
if(doc.disabled) {
return [__("Disabled"), "red"];
} else if(doc.next_schedule_date >= frappe.datetime.get_today() && doc.status != 'Stopped') {
return [__("Active"), "green"];
} else if(doc.docstatus === 0) {
return [__("Draft"), "red", "docstatus,=,0"];
} else if(doc.status === 'Stopped') {
return [__("Stopped"), "red"];
} else {
return [__("Expired"), "darkgrey"];
}
}
};

View file

@ -684,4 +684,4 @@
"track_changes": 1,
"track_seen": 1,
"track_views": 0
}
}

View file

@ -3,8 +3,6 @@
from __future__ import unicode_literals
import frappe
import json
@frappe.whitelist()
def get_list_settings(doctype):
@ -22,31 +20,37 @@ def set_list_settings(doctype, values):
doc = frappe.new_doc("List View Setting")
doc.name = doctype
frappe.clear_messages()
doc.update(json.loads(values))
doc.update(frappe.parse_json(values))
doc.save()
@frappe.whitelist()
def get_user_assignments_and_count(doctype, current_filters):
def get_group_by_count(doctype, current_filters, field):
current_filters = frappe.parse_json(current_filters)
subquery_condition = ''
if current_filters:
# get the subquery
subquery = frappe.get_all(doctype,
filters=current_filters, return_query = True)
subquery = frappe.get_all(doctype, filters=current_filters, return_query = True)
if field == 'assigned_to':
subquery_condition = ' and `tabToDo`.reference_name in ({subquery})'.format(subquery = subquery)
return frappe.db.sql("""select `tabToDo`.owner as name, count(*) as count
from
`tabToDo`, `tabUser`
where
`tabToDo`.status='Open' and
`tabToDo`.owner = `tabUser`.name and
`tabUser`.user_type = 'System User'
{subquery_condition}
group by
`tabToDo`.owner
order by
count desc
limit 50""".format(subquery_condition = subquery_condition), as_dict=True)
else :
return frappe.db.get_list(doctype,
filters=current_filters,
group_by=field,
fields=['count(*) as count', field + ' as name'],
order_by='count desc',
limit=50,
)
todo_list = frappe.db.sql("""select `tabToDo`.owner as name, count(*) as count
from
`tabToDo`, `tabUser`
where
`tabToDo`.status='Open' and
`tabToDo`.owner = `tabUser`.name and
`tabUser`.user_type = 'System User'
{subquery_condition}
group by
`tabToDo`.owner
order by
count desc
limit 50""".format(subquery_condition = subquery_condition), as_dict=True)
return todo_list

View file

@ -287,44 +287,141 @@ def get_onboard_items(app, module):
return onboard_items or fallback_items
@frappe.whitelist()
def get_links_for_module(app, module):
return [l.get('label') for l in get_links(app, module)]
def get_links(app, module):
try:
sections = get_config(app, frappe.scrub(module))
except ImportError:
return []
link_names = []
links = []
for section in sections:
for item in section["items"]:
link_names.append(item.get("label"))
return link_names
for item in section['items']:
links.append(item)
return links
@frappe.whitelist()
def hide_modules_from_desktop(modules):
def get_desktop_settings():
from frappe.config import get_modules_from_all_apps_for_user
all_modules = get_modules_from_all_apps_for_user()
home_settings = get_home_settings()
modules_by_name = {}
for m in all_modules:
modules_by_name[m['module_name']] = m
module_categories = ['Modules', 'Domains', 'Places', 'Administration']
user_modules_by_category = {}
user_saved_modules_by_category = home_settings.modules_by_category or {}
user_saved_links_by_module = home_settings.links_by_module or {}
def apply_user_saved_links(module):
module = frappe._dict(module)
all_links = get_links(module.app, module.module_name)
module_links_by_label = {}
for link in all_links:
module_links_by_label[link['label']] = link
if module.module_name in user_saved_links_by_module:
user_links = frappe.parse_json(user_saved_links_by_module[module.module_name])
module.links = [module_links_by_label[l] for l in user_links if l in module_links_by_label]
return module
for category in module_categories:
if category in user_saved_modules_by_category:
user_modules = user_saved_modules_by_category[category]
user_modules_by_category[category] = [apply_user_saved_links(modules_by_name[m]) \
for m in user_modules]
else:
user_modules_by_category[category] = [apply_user_saved_links(m) \
for m in all_modules if m.get('category') == category]
# filter out hidden modules
if home_settings.hidden_modules:
for category in user_modules_by_category:
hidden_modules = home_settings.hidden_modules or []
modules = user_modules_by_category[category]
user_modules_by_category[category] = [module for module in modules if module.module_name not in hidden_modules]
return user_modules_by_category
@frappe.whitelist()
def update_hidden_modules(category_map):
category_map = frappe.parse_json(category_map)
home_settings = get_home_settings()
saved_hidden_modules = home_settings.hidden_modules or []
for category in category_map:
config = frappe._dict(category_map[category])
saved_hidden_modules += config.removed or []
saved_hidden_modules = [d for d in saved_hidden_modules if d not in (config.added or [])]
home_settings.hidden_modules = saved_hidden_modules
set_home_settings(home_settings)
return get_desktop_settings()
@frappe.whitelist()
def update_modules_order(module_category, modules):
modules = frappe.parse_json(modules)
home_settings = frappe.db.get_value("User", frappe.session.user, 'home_settings')
home_settings = frappe.parse_json(home_settings or '{}')
home_settings['hidden_modules'] = modules
frappe.db.set_value('User', frappe.session.user, 'home_settings', json.dumps(home_settings))
return home_settings
home_settings = get_home_settings()
home_settings.modules_by_category = home_settings.modules_by_category or {}
home_settings.modules_by_category[module_category] = modules
set_home_settings(home_settings)
@frappe.whitelist()
def update_links_for_module(module_name, links):
home_settings = frappe.db.get_value("User", frappe.session.user, 'home_settings')
home_settings = frappe.parse_json(home_settings or '{}')
links = frappe.parse_json(links)
home_settings = get_home_settings()
home_settings.setdefault('links', {})
home_settings['links'].setdefault(module_name, None)
home_settings['links'][module_name] = links
home_settings.setdefault('links_by_module', {})
home_settings['links_by_module'].setdefault(module_name, None)
home_settings['links_by_module'][module_name] = links
set_home_settings(home_settings)
return get_desktop_settings()
@frappe.whitelist()
def get_options_for_show_hide_cards():
from frappe.config import get_modules_from_all_apps_for_user
all_modules = get_modules_from_all_apps_for_user()
home_settings = get_home_settings()
hidden_modules = home_settings.hidden_modules or []
options = []
for module in all_modules:
module = frappe._dict(module)
options.append({
'category': module.category,
'label': module.label,
'value': module.module_name,
'checked': module.module_name not in hidden_modules
})
return options
def set_home_settings(home_settings):
frappe.cache().hset('home_settings', frappe.session.user, home_settings)
frappe.db.set_value('User', frappe.session.user, 'home_settings', json.dumps(home_settings))
@frappe.whitelist()
def get_home_settings():
def get_from_db():
settings = frappe.db.get_value("User", frappe.session.user, 'home_settings')
return frappe.parse_json(settings or '{}')
home_settings = frappe.cache().hget('home_settings', frappe.session.user, get_from_db)
return home_settings

View file

@ -307,6 +307,7 @@ def export_query():
if isinstance(data.get("file_format_type"), string_types):
file_format_type = data["file_format_type"]
include_indentation = data["include_indentation"]
if isinstance(data.get("visible_idx"), string_types):
visible_idx = json.loads(data.get("visible_idx"))
else:
@ -318,7 +319,7 @@ def export_query():
columns = get_columns_dict(data.columns)
from frappe.utils.xlsxutils import make_xlsx
xlsx_data = build_xlsx_data(columns, data, visible_idx)
xlsx_data = build_xlsx_data(columns, data, visible_idx, include_indentation)
xlsx_file = make_xlsx(xlsx_data, "Query Report")
frappe.response['filename'] = report_name + '.xlsx'
@ -326,7 +327,7 @@ def export_query():
frappe.response['type'] = 'binary'
def build_xlsx_data(columns, data, visible_idx):
def build_xlsx_data(columns, data, visible_idx,include_indentation):
result = [[]]
# add column headings
@ -344,7 +345,7 @@ def build_xlsx_data(columns, data, visible_idx):
label = columns[idx]["label"]
fieldname = columns[idx]["fieldname"]
cell_value = row.get(fieldname, row.get(label, ""))
if 'indent' in row and idx == 0:
if cint(include_indentation) and 'indent' in row and idx == 0:
cell_value = (' ' * cint(row['indent'])) + cell_value
row_data.append(cell_value)
else:

View file

@ -185,6 +185,10 @@ def append_totals_row(data):
for i in range(len(row)):
if isinstance(row[i], (float, int)):
totals[i] = (totals[i] or 0) + row[i]
if not isinstance(totals[0], (int, float)):
totals[0] = 'Total'
data.append(totals)
return data

File diff suppressed because it is too large Load diff

View file

@ -161,7 +161,6 @@ scheduler_events = {
"frappe.desk.page.backups.backups.delete_downloadable_backups",
"frappe.limits.update_space_usage",
"frappe.limits.update_site_usage",
"frappe.desk.doctype.auto_repeat.auto_repeat.make_auto_repeat_entry",
"frappe.deferred_insert.save_to_db",
"frappe.desk.form.document_follow.send_hourly_updates",
],
@ -181,6 +180,8 @@ scheduler_events = {
"frappe.desk.form.document_follow.send_daily_updates",
"frappe.social.doctype.energy_point_settings.energy_point_settings.allocate_review_points",
"frappe.integrations.doctype.google_contacts.google_contacts.sync",
"frappe.automation.doctype.auto_repeat.auto_repeat.make_auto_repeat_entry",
"frappe.automation.doctype.auto_repeat.auto_repeat.set_auto_repeat_as_completed"
],
"daily_long": [
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",
@ -236,6 +237,7 @@ setup_wizard_exception = "frappe.desk.page.setup_wizard.setup_wizard.email_setup
before_write_file = "frappe.limits.validate_space_limit"
before_migrate = ['frappe.patches.v11_0.sync_user_permission_doctype_before_migrate.execute']
after_migrate = ['frappe.website.doctype.website_theme.website_theme.generate_theme_files_if_not_exist']
otp_methods = ['OTP App','Email','SMS']
user_privacy_documents = [

View file

@ -1,5 +0,0 @@
// Copyright (c) 2017, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Google Maps Settings', {
});

View file

@ -1,159 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-10-16 17:13:05.684227",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "enabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Enabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "client_key",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Client Key",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "home_address",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Home Address",
"length": 0,
"no_copy": 0,
"options": "Address",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2018-08-21 14:53:09.170463",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Maps Settings",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View file

@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
class GoogleMapsSettings(Document):
def validate(self):
if self.enabled:
if not self.client_key:
frappe.throw(_("Client key is required"))
if not self.home_address:
frappe.throw(_("Home Address is required"))
def get_client(self):
if not self.enabled:
frappe.throw(_("Google Maps integration is not enabled"))
import googlemaps
try:
client = googlemaps.Client(key=self.client_key)
except Exception as e:
frappe.throw(e.message)
return client

View file

@ -1,23 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Google Maps Settings", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Google Maps
() => frappe.tests.make('Google Maps Settings', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View file

@ -1,8 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
class TestGoogleMapsSettings(unittest.TestCase):
pass

View file

@ -6,7 +6,8 @@
"enable",
"google_credentials",
"client_id",
"client_secret"
"client_secret",
"api_key"
],
"fields": [
{
@ -32,10 +33,15 @@
"fieldtype": "Password",
"in_list_view": 1,
"label": "Client Secret"
},
{
"fieldname": "api_key",
"fieldtype": "Data",
"label": "API Key"
}
],
"issingle": 1,
"modified": "2019-06-19 15:28:05.957380",
"modified": "2019-06-29 13:26:33.201060",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Settings",

View file

@ -0,0 +1,38 @@
{
"creation": "2019-05-29 01:24:29.585060",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"ldap_group",
"erpnext_role"
],
"fields": [
{
"fieldname": "ldap_group",
"fieldtype": "Data",
"in_list_view": 1,
"label": "LDAP Group",
"reqd": 1
},
{
"fieldname": "erpnext_role",
"fieldtype": "Link",
"in_list_view": 1,
"label": "ERPNext Role",
"options": "Role",
"reqd": 1
}
],
"istable": 1,
"modified": "2019-07-15 06:46:38.050408",
"modified_by": "Administrator",
"module": "Integrations",
"name": "LDAP Group Mapping",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class LDAPGroupMapping(Document):
pass

View file

@ -1,594 +1,215 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-09-22 04:16:48.829658",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "System",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enabled",
"ldap_server_url",
"column_break_4",
"base_dn",
"password",
"section_break_5",
"organizational_unit",
"default_role",
"ldap_search_string",
"ldap_email_field",
"ldap_username_field",
"column_break_11",
"ldap_first_name_field",
"ldap_middle_name_field",
"ldap_last_name_field",
"ldap_phone_field",
"ldap_mobile_field",
"ldap_security",
"ssl_tls_mode",
"require_trusted_certificate",
"column_break_17",
"local_private_key_file",
"local_server_certificate_file",
"local_ca_certs_file",
"ldap_group_mappings_section",
"ldap_group_field",
"ldap_groups"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Enabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Enabled"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "ldap_server_url",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "LDAP Server Url",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "organizational_unit",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Organizational Unit",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "base_dn",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Base Distinguished Name (DN)",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "password",
"fieldtype": "Password",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Password for Base DN",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "LDAP User Creation and Mapping"
},
{
"fieldname": "organizational_unit",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Organizational Unit for Users",
"reqd": 1
},
{
"fieldname": "default_role",
"fieldtype": "Link",
"label": "Default Role on Creation",
"options": "Role",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "ldap_search_string",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "LDAP Search String",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "ldap_first_name_field",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "LDAP First Name Field",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "ldap_email_field",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "LDAP Email Field",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "ldap_username_field",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "LDAP Username Field",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "ldap_first_name_field",
"fieldtype": "Data",
"label": "LDAP First Name Field",
"reqd": 1
},
{
"fieldname": "ldap_middle_name_field",
"fieldtype": "Data",
"label": "LDAP Middle Name Field"
},
{
"fieldname": "ldap_last_name_field",
"fieldtype": "Data",
"label": "LDAP Last Name Field"
},
{
"fieldname": "ldap_phone_field",
"fieldtype": "Data",
"label": "LDAP Phone Field"
},
{
"fieldname": "ldap_mobile_field",
"fieldtype": "Data",
"label": "LDAP Mobile Field"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "ldap_security",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "LDAP Security",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "LDAP Security"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Off",
"description": "",
"fetch_if_empty": 0,
"fieldname": "ssl_tls_mode",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "SSL/TLS Mode",
"length": 0,
"no_copy": 0,
"options": "Off\nStartTLS",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Off\nStartTLS"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "No",
"fetch_if_empty": 0,
"fieldname": "require_trusted_certificate",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Require Trusted Certificate",
"length": 0,
"no_copy": 0,
"options": "No\nYes",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "local_private_key_file",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Path to private Key File",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Path to private Key File"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "local_server_certificate_file",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Path to Server Certificate",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Path to Server Certificate"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "local_ca_certs_file",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Path to CA Certs File",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Path to CA Certs File"
},
{
"fieldname": "ldap_group_mappings_section",
"fieldtype": "Section Break",
"label": "LDAP Group Mappings"
},
{
"fieldname": "ldap_group_field",
"fieldtype": "Data",
"label": "LDAP Group Field"
},
{
"fieldname": "ldap_groups",
"fieldtype": "Table",
"label": "LDAP Group Mappings",
"options": "LDAP Group Mapping"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2019-04-29 10:56:42.322696",
"modified": "2019-07-15 06:48:16.562109",
"modified_by": "Administrator",
"module": "Integrations",
"name": "LDAP Settings",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 1,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

View file

@ -15,155 +15,195 @@ class LDAPSettings(Document):
if not self.flags.ignore_mandatory:
if self.ldap_search_string and self.ldap_search_string.endswith("={0}"):
connect_to_ldap(server_url=self.ldap_server_url,
base_dn=self.base_dn,
password=self.get_password(raise_exception=False),
ssl_tls_mode=self.ssl_tls_mode,
trusted_cert=self.require_trusted_certificate,
private_key_file=self.local_private_key_file,
server_cert_file=self.local_server_certificate_file,
ca_certs_file=self.local_ca_certs_file
)
self.connect_to_ldap(base_dn=self.base_dn, password=self.get_password(raise_exception=False))
else:
frappe.throw(_("LDAP Search String needs to end with a placeholder, eg sAMAccountName={0}"))
def connect_to_ldap(self, base_dn, password):
try:
import ldap3
import ssl
def get_ldap_client_settings():
#return the settings to be used on the client side.
result = {
"enabled": False
}
settings = frappe.get_doc("LDAP Settings")
if self.require_trusted_certificate == 'Yes':
tls_configuration = ldap3.Tls(validate=ssl.CERT_REQUIRED, version=ssl.PROTOCOL_TLSv1)
else:
tls_configuration = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1)
if settings and settings.enabled:
result["enabled"] = True
result["method"] = "frappe.integrations.doctype.ldap_settings.ldap_settings.login"
return result
if self.local_private_key_file:
tls_configuration.private_key_file = self.local_private_key_file
if self.local_server_certificate_file:
tls_configuration.certificate_file = self.local_server_certificate_file
if self.local_ca_certs_file:
tls_configuration.ca_certs_file = self.local_ca_certs_file
server = ldap3.Server(host=self.ldap_server_url, tls=tls_configuration)
bind_type = ldap3.AUTO_BIND_TLS_BEFORE_BIND if self.ssl_tls_mode == "StartTLS" else True
def connect_to_ldap(server_url,
base_dn,
password,
ssl_tls_mode,
trusted_cert,
private_key_file,
server_cert_file,
ca_certs_file):
try:
import ldap3
import ssl
conn = ldap3.Connection(
server=server,
user=base_dn,
password=password,
auto_bind=bind_type,
read_only=True,
raise_exceptions=True)
if trusted_cert == 'Yes':
tls_configuration = ldap3.Tls(validate=ssl.CERT_REQUIRED,
version=ssl.PROTOCOL_TLSv1)
return conn
except ImportError:
msg = _("Please Install the ldap3 library via pip to use ldap functionality.")
frappe.throw(msg, title=_("LDAP Not Installed"))
except ldap3.core.exceptions.LDAPInvalidCredentialsResult:
frappe.throw(_("Invalid username or password"))
except Exception as ex:
frappe.throw(_(str(ex)))
@staticmethod
def get_ldap_client_settings():
# return the settings to be used on the client side.
result = {
"enabled": False
}
ldap = frappe.get_doc("LDAP Settings")
if ldap.enabled:
result["enabled"] = True
result["method"] = "frappe.integrations.doctype.ldap_settings.ldap_settings.login"
return result
@classmethod
def update_user_fields(cls, user, user_data):
updatable_data = {key: value for key, value in user_data.items() if key != 'email'}
for key, value in updatable_data.items():
setattr(user, key, value)
user.save(ignore_permissions=True)
def sync_roles(self, user, additional_groups=None):
current_roles = set([d.role for d in user.get("roles")])
needed_roles = set()
needed_roles.add(self.default_role)
lower_groups = [g.lower() for g in additional_groups or []]
all_mapped_roles = {r.erpnext_role for r in self.ldap_groups}
matched_roles = {r.erpnext_role for r in self.ldap_groups if r.ldap_group.lower() in lower_groups}
unmatched_roles = all_mapped_roles.difference(matched_roles)
needed_roles.update(matched_roles)
roles_to_remove = current_roles.intersection(unmatched_roles)
if not needed_roles.issubset(current_roles):
missing_roles = needed_roles.difference(current_roles)
user.add_roles(*missing_roles)
user.remove_roles(*roles_to_remove)
def create_or_update_user(self, user_data, groups=None):
user = None
if frappe.db.exists("User", user_data['email']):
user = frappe.get_doc("User", user_data['email'])
LDAPSettings.update_user_fields(user=user, user_data=user_data)
else:
tls_configuration = ldap3.Tls(validate=ssl.CERT_NONE,
version=ssl.PROTOCOL_TLSv1)
doc = user_data
doc.update({
"doctype": "User",
"send_welcome_email": 0,
"language": "",
"user_type": "System User",
# "roles": [{
# "role": self.default_role
# }]
})
user = frappe.get_doc(doc)
user.insert(ignore_permissions=True)
# always add default role.
user.add_roles(self.default_role)
if self.ldap_group_field:
self.sync_roles(user, groups)
return user
if private_key_file:
tls_configuration.private_key_file = private_key_file
if server_cert_file:
tls_configuration.certificate_file = server_cert_file
if ca_certs_file:
tls_configuration.ca_certs_file = ca_certs_file
def get_ldap_attributes(self):
ldap_attributes = [self.ldap_email_field, self.ldap_username_field, self.ldap_first_name_field]
server = ldap3.Server(host=server_url,
tls=tls_configuration)
bind_type = ldap3.AUTO_BIND_TLS_BEFORE_BIND if ssl_tls_mode == "StartTLS" else True
if self.ldap_group_field:
ldap_attributes.append(self.ldap_group_field)
conn = ldap3.Connection(server=server,
user=base_dn,
password=password,
auto_bind=bind_type,
read_only=True,
raise_exceptions=True)
if self.ldap_middle_name_field:
ldap_attributes.append(self.ldap_middle_name_field)
return conn
if self.ldap_last_name_field:
ldap_attributes.append(self.ldap_last_name_field)
except ImportError:
msg = _("Please Install the ldap3 library via pip to use ldap functionality.")
frappe.throw(msg, title=_("LDAP Not Installed"))
except ldap3.core.exceptions.LDAPInvalidCredentialsResult:
frappe.throw(_("Invalid Credentials"))
except Exception as ex:
frappe.throw(_(str(ex)))
if self.ldap_phone_field:
ldap_attributes.append(self.ldap_phone_field)
if self.ldap_mobile_field:
ldap_attributes.append(self.ldap_mobile_field)
return ldap_attributes
def authenticate(self, username, password):
if not self.enabled:
frappe.throw(_("LDAP is not enabled."))
user_filter = self.ldap_search_string.format(username)
ldap_attributes = self.get_ldap_attributes()
conn = self.connect_to_ldap(self.base_dn, self.get_password(raise_exception=False))
conn.search(
search_base=self.organizational_unit,
search_filter="({0})".format(user_filter),
attributes=ldap_attributes)
if len(conn.entries) == 1 and conn.entries[0]:
user = conn.entries[0]
# only try and connect as the user, once we have their fqdn entry.
self.connect_to_ldap(base_dn=user.entry_dn, password=password)
groups = None
if self.ldap_group_field:
groups = getattr(user, self.ldap_group_field).values
return self.create_or_update_user(self.convert_ldap_entry_to_dict(user), groups=groups)
else:
frappe.throw(_("Invalid username or password"))
def convert_ldap_entry_to_dict(self, user_entry):
data = {
'username': user_entry[self.ldap_username_field].value,
'email': user_entry[self.ldap_email_field].value,
'first_name': user_entry[self.ldap_first_name_field].value
}
# optional fields
if self.ldap_middle_name_field:
data['middle_name'] = user_entry[self.ldap_middle_name_field].value
if self.ldap_last_name_field:
data['last_name'] = user_entry[self.ldap_last_name_field].value
if self.ldap_phone_field:
data['phone'] = user_entry[self.ldap_phone_field].value
if self.ldap_mobile_field:
data['mobile_no'] = user_entry[self.ldap_mobile_field].value
return data
@frappe.whitelist(allow_guest=True)
def login():
# LDAP LOGIN LOGIC
args = frappe.form_dict
user = authenticate_ldap_user(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd))
ldap = frappe.get_doc("LDAP Settings")
user = ldap.authenticate(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd))
frappe.local.login_manager.user = user.name
frappe.local.login_manager.post_login()
# because of a GET request!
frappe.db.commit()
def authenticate_ldap_user(user=None,
password=None):
params = {}
settings = frappe.get_doc("LDAP Settings")
if settings and settings.enabled:
conn = connect_to_ldap(server_url=settings.ldap_server_url,
base_dn=settings.base_dn,
password=settings.get_password(raise_exception=False),
ssl_tls_mode=settings.ssl_tls_mode,
trusted_cert=settings.require_trusted_certificate,
private_key_file=settings.local_private_key_file,
server_cert_file=settings.local_server_certificate_file,
ca_certs_file=settings.local_ca_certs_file)
user_filter = settings.ldap_search_string.format(user)
conn.search(search_base=settings.organizational_unit,
search_filter="({0})".format(user_filter),
attributes=[settings.ldap_email_field,
settings.ldap_username_field,
settings.ldap_first_name_field])
if len(conn.entries) > 0 and conn.entries[0]:
user = conn.entries[0]
params["email"] = str(user[settings.ldap_email_field])
params["username"] = str(user[settings.ldap_username_field])
params["first_name"] = str(user[settings.ldap_first_name_field])
connect_to_ldap(server_url=settings.ldap_server_url,
base_dn=user.entry_dn,
password=frappe.as_unicode(password),
ssl_tls_mode=settings.ssl_tls_mode,
trusted_cert=settings.require_trusted_certificate,
private_key_file=settings.local_private_key_file,
server_cert_file=settings.local_server_certificate_file,
ca_certs_file=settings.local_ca_certs_file
)
return create_user(params)
else:
frappe.throw(_("Not a valid LDAP user"))
else:
frappe.throw(_("LDAP is not enabled."))
def create_user(params):
if frappe.db.exists("User", params["email"]):
user = frappe.get_doc("User", params["email"])
user.first_name = params["first_name"]
user.username = params["username"]
user.save(ignore_permissions=True)
return user
else:
params.update({
"doctype": "User",
"send_welcome_email": 0,
"language": "",
"user_type": "System User",
"roles": [{
"role": _("Customer")
}]
})
user = frappe.get_doc(params).insert(ignore_permissions=True)
return user

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestLDAPSettings(unittest.TestCase):
pass

View file

@ -66,15 +66,26 @@ def take_backups_if(freq):
@frappe.whitelist()
def take_backups_s3():
def take_backups_s3(retry_count=0):
try:
backup_to_s3()
send_email(True, "S3 Backup Settings")
except JobTimeoutException:
if retry_count < 2:
args = {
"retry_count" :retry_count + 1
}
enqueue("frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3",
queue='long', timeout=1500, **args)
else:
notify()
except Exception:
error_message = frappe.get_traceback()
frappe.errprint(error_message)
send_email(False, "S3 Backup Settings", error_message)
notify()
def notify():
error_message = frappe.get_traceback()
frappe.errprint(error_message)
send_email(False, "S3 Backup Settings", error_message)
def send_email(success, service_name, error_status=None):
if success:
@ -134,6 +145,7 @@ def upload_file_to_s3(filename, folder, conn, bucket):
conn.upload_file(filename, bucket, destpath)
except Exception as e:
frappe.log_error()
print("Error uploading: %s" % (e))

View file

@ -477,7 +477,7 @@ def get_field_currency(df, doc=None):
if ":" in cstr(df.get("options")):
split_opts = df.get("options").split(":")
if len(split_opts)==3:
if len(split_opts)==3 and doc.get(split_opts[1]):
currency = frappe.get_cached_value(split_opts[0], doc.get(split_opts[1]), split_opts[2])
else:
currency = doc.get(df.get("options"))

View file

@ -237,7 +237,7 @@ frappe.patches.v12_0.set_primary_key_in_series
execute:frappe.delete_doc("Page", "modules", ignore_missing=True)
frappe.patches.v11_0.set_default_letter_head_source
frappe.patches.v12_0.setup_comments_from_communications
frappe.patches.v12_0.init_desk_settings #11-03-2019
frappe.patches.v12_0.init_desk_settings #16-05-2019
frappe.patches.v12_0.replace_null_values_in_tables
frappe.patches.v12_0.reset_home_settings
frappe.patches.v12_0.update_print_format_type
@ -246,3 +246,4 @@ frappe.patches.v11_0.apply_customization_to_custom_doctype
frappe.patches.v12_0.remove_feedback_rating
frappe.patches.v12_0.move_form_attachments_to_attachments_folder
frappe.patches.v12_0.move_timeline_links_to_dynamic_links
frappe.patches.v12_0.delete_feedback_request_if_exists #1

View file

@ -0,0 +1,8 @@
import frappe
def execute():
frappe.db.sql('''
DELETE from `tabDocType`
WHERE name = 'Feedback Request'
''')

View file

@ -8,4 +8,4 @@ from frappe.desk.moduleview import get_onboard_items
def execute():
"""Reset the initial customizations for desk, with modules, indices and links."""
frappe.reload_doc("core", "doctype", "user")
frappe.db.sql("""update `tabUser` set home_settings = %s""", (''), debug=True)
frappe.db.sql("""update tabUser set home_settings = ''""")

View file

@ -0,0 +1,9 @@
import frappe
def execute():
# convert all /path to path
frappe.db.sql('''
UPDATE `tabWebsite Meta Tag`
SET parent = SUBSTR(parent, 2)
WHERE parent like '/%'
''')

View file

@ -25,7 +25,7 @@ def migrate_style_settings():
website_theme.no_sidebar = cint(frappe.db.get_single_value("Website Settings", "no_sidebar"))
website_theme.save()
website_theme.use_theme()
website_theme.set_as_default()
def map_color_fields(style_settings, website_theme):
color_fields_map = {

View file

@ -36,7 +36,7 @@
"public/js/frappe/ui/messages.js",
"public/js/frappe/translate.js",
"public/js/frappe/utils/pretty_date.js",
"public/js/lib/microtemplate.js",
"public/js/frappe/microtemplate.js",
"public/js/frappe/query_string.js",
"public/js/frappe/ui/dropzone.js",
@ -145,7 +145,7 @@
"public/js/frappe/router_history.js",
"public/js/frappe/defaults.js",
"public/js/frappe/roles_editor.js",
"public/js/lib/microtemplate.js",
"public/js/frappe/microtemplate.js",
"public/js/legacy/handler.js",
@ -276,6 +276,7 @@
"public/js/frappe/list/list_sidebar.js",
"public/js/frappe/list/list_sidebar.html",
"public/js/frappe/list/list_sidebar_stat.html",
"public/js/frappe/list/list_sidebar_group_by.js",
"public/js/frappe/list/list_view_permission_restrictions.html",
"public/js/frappe/views/gantt/gantt_view.js",

View file

@ -464,6 +464,11 @@ frappe.Application = Class.extend({
return frappe.call('frappe.client.get_hooks', { hook: 'app_logo_url' })
.then(r => {
frappe.app.logo_url = (r.message || []).slice(-1)[0];
if (window.cordova) {
let host = frappe.request.url;
host = host.slice(0, host.length - 1);
frappe.app.logo_url = host + frappe.app.logo_url;
}
});
},

View file

@ -309,9 +309,8 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
function get_filter_description(filter) {
let doctype = filter[0];
let fieldname = filter[1];
let label = meta
? frappe.meta.get_docfield(doctype, fieldname).label
: frappe.model.unscrub(fieldname);
let docfield = frappe.meta.get_docfield(doctype, fieldname);
let label = docfield ? docfield.label : frappe.model.unscrub(fieldname);
let value = filter[3] == null || filter[3] === ''
? __('empty')

View file

@ -69,6 +69,8 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({
this.set_input_attributes();
this.values = [];
this._options = [];
this._selected_values = [];
this.highlighted = -1;
},
@ -104,6 +106,20 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({
this.update_status();
},
set_value(value) {
if (!value) return Promise.resolve();
if (typeof value === 'string') {
value = [value];
}
this.values = value;
this.values.forEach(value => {
this.update_selected_values(value);
});
this.parse_validate_and_set_in_model('');
this.update_status();
return Promise.resolve();
},
update_selected_values(value) {
this._selected_values = this._selected_values || [];
let option = this._options.find(opt => opt.value === value);
@ -122,7 +138,8 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({
text = this.get_placeholder_text();
} else if (this.values.length === 1) {
let val = this.values[0];
text = this._options.find(opt => opt.value === val).label;
let option = this._options.find(opt => opt.value === val);
text = option ? option.label : val;
} else {
text = __('{0} values selected', [this.values.length]);
}

View file

@ -120,14 +120,16 @@ frappe.ui.form.Form = class FrappeForm {
shortcut: 'shift+>',
action: () => this.navigate_records(0),
page: this.page,
description: __('Go to next record')
description: __('Go to next record'),
condition: () => !this.is_new()
});
frappe.ui.keys.add_shortcut({
shortcut: 'shift+<',
action: () => this.navigate_records(1),
page: this.page,
description: __('Go to previous record')
description: __('Go to previous record'),
condition: () => !this.is_new()
});
}
@ -540,7 +542,9 @@ frappe.ui.form.Form = class FrappeForm {
me.script_manager.trigger("after_save");
// submit comment if entered
me.timeline.comment_area.submit();
if (me.timeline) {
me.timeline.comment_area.submit();
}
me.refresh();
} else {
if(on_error) {

View file

@ -606,7 +606,7 @@ export default class GridRow {
}
}
get_visible_columns(blacklist) {
get_visible_columns(blacklist=[]) {
var me = this;
var visible_columns = $.map(this.docfields, function(df) {
var visible = !df.hidden && df.in_list_view && me.grid.frm.get_perm(df.permlevel, "read")

View file

@ -35,6 +35,20 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
if(!this.date_field) {
this.date_field = "transaction_date";
}
// setters can be defined as a dict or a list of fields
// setters define the additional filters that get applied
// for selection
// CASE 1: DocType name and fieldname is the same, example "customer" and "customer"
// setters define the filters applied in the modal
// if the fieldnames and doctypes are consistently named,
// pass a dict with the setter key and value, for example
// {customer: [customer_name]}
// CASE 2: if the fieldname of the target is different,
// then pass a list of fields with appropriate fieldname
if($.isArray(this.setters)) {
for (let df of this.setters) {
fields.push(df, {fieldtype: "Column Break"});
@ -142,6 +156,7 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
clearTimeout($this.data('timeout'));
$this.data('timeout', setTimeout(function() {
frappe.flags.auto_scroll = false;
me.empty_list();
me.get_results();
}, 300));
});
@ -198,16 +213,15 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
render_result_list: function(results, more = 0) {
var me = this;
var more_btn = me.dialog.fields_dict.more_btn.$wrapper;
// Make empty result set if filter is set
if (!frappe.flags.auto_scroll) {
this.$results.splice(1, this.$results.length);
this.empty_list();
}
if(results.length === 0) {
this.$results.empty();
this.empty_list();
more_btn.hide();
return;
} else if(more) {
@ -223,6 +237,10 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
}
},
empty_list: function() {
this.$results.find('.list-item-container').remove();
},
get_results: function() {
let me = this;

View file

@ -484,7 +484,7 @@ frappe.ui.get_print_settings = function (pdf, callback, letter_head) {
default: "Landscape"
}];
frappe.prompt(columns, function (data) {
return frappe.prompt(columns, function (data) {
var data = $.extend(print_settings, data);
if (!data.with_letter_head) {
data.letter_head = null;

View file

@ -159,17 +159,29 @@ frappe.ui.form.QuickEntryForm = Class.extend({
doc: me.dialog.doc
},
callback: function(r) {
me.dialog.hide();
// delete the old doc
frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name);
me.dialog.doc = r.message;
if(frappe._from_link) {
frappe.ui.form.update_calling_link(me.dialog.doc);
if (frappe.model.is_submittable(me.doctype)) {
frappe.run_serially([
() => me.dialog.working = true,
() => {
me.dialog.set_primary_action(__('Submit'), function() {
me.submit(r.message);
});
}
]);
} else {
if(me.after_insert) {
me.after_insert(me.dialog.doc);
me.dialog.hide();
// delete the old doc
frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name);
me.dialog.doc = r.message;
if(frappe._from_link) {
frappe.ui.form.update_calling_link(me.dialog.doc);
} else {
me.open_form_if_not_list();
if(me.after_insert) {
me.after_insert(me.dialog.doc);
} else {
me.open_form_if_not_list();
}
}
}
},
@ -185,6 +197,26 @@ frappe.ui.form.QuickEntryForm = Class.extend({
});
},
submit: function(doc) {
var me = this;
frappe.call({
method: "frappe.client.submit",
args : {
doc: doc
},
callback: function(r) {
me.dialog.hide();
// delete the old doc
frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name);
me.dialog.doc = r.message;
if (frappe._from_link) {
frappe.ui.form.update_calling_link(me.dialog.doc);
}
cur_frm.reload_doc();
}
});
},
open_form_if_not_list: function() {
let route = frappe.get_route();
let doc = this.dialog.doc;

View file

@ -142,13 +142,16 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
}
});
if (frm.is_new() && frm.meta.autoname === 'Prompt' && !frm.doc.__newname) {
error_fields = [__('Name'), ...error_fields];
}
if (error_fields.length) {
if (doc.parenttype) {
var message = __('Mandatory fields required in table {0}, Row {1}',
[__(frappe.meta.docfield_map[doc.parenttype][doc.parentfield].label).bold(), doc.idx]);
} else {
var message = __('Mandatory fields required in {0}', [__(doc.doctype)]);
}
message = message + '<br><br><ul><li>' + error_fields.join('</li><li>') + "</ul>";
frappe.msgprint({

View file

@ -169,7 +169,7 @@ frappe.ui.form.ScriptManager = Class.extend({
function setup_add_fetch(df) {
if((['Data', 'Read Only', 'Text', 'Small Text', 'Currency',
'Text Editor', 'Code', 'Link', 'Float', 'Int', 'Date'].includes(df.fieldtype) || df.read_only==1)
'Text Editor', 'Code', 'Link', 'Float', 'Int', 'Date', 'Select'].includes(df.fieldtype) || df.read_only==1)
&& df.fetch_from && df.fetch_from.indexOf(".")!=-1) {
var parts = df.fetch_from.split(".");
me.frm.add_fetch(parts[0], parts[1], df.fieldname);

View file

@ -36,6 +36,7 @@ frappe.ui.form.Sidebar = Class.extend({
this.bind_events();
this.setup_keyboard_shortcuts();
this.show_auto_repeat_status();
frappe.ui.form.setup_user_image_event(this.frm);
this.refresh();
@ -88,6 +89,28 @@ frappe.ui.form.Sidebar = Class.extend({
}
},
show_auto_repeat_status: function() {
if (this.frm.meta.allow_auto_repeat && this.frm.doc.auto_repeat) {
const me = this;
frappe.call({
method: "frappe.client.get_value",
args:{
doctype: "Auto Repeat",
filters: {
name: this.frm.doc.auto_repeat
},
fieldname: ["frequency"]
},
callback: function(res) {
me.sidebar.find(".auto-repeat-status").html(__("Repeats {0}", [res.message.frequency]));
me.sidebar.find(".auto-repeat-status").on("click", function(){
frappe.set_route("Form", "Auto Repeat", me.frm.doc.auto_repeat);
});
}
});
}
},
refresh_comments: function() {
$.map(this.frm.timeline.get_communications(), function(c) {
return (c.communication_type==="Communication" || (c.communication_type=="Comment" && c.comment_type==="Comment")) ? c : null;

View file

@ -4,6 +4,7 @@ frappe.ui.form.set_user_image = function(frm) {
var image_field = frm.meta.image_field;
var image = frm.doc[image_field];
var title_image = frm.page.$title_area.find('.title-image');
var image_actions = frm.sidebar.image_wrapper.find('.sidebar-image-actions');
image_section.toggleClass('hide', image_field ? false : true);
@ -32,6 +33,8 @@ frappe.ui.form.set_user_image = function(frm) {
.css("background-image", 'url("' + image + '")')
.html('');
image_actions.find('.sidebar-image-change, .sidebar-image-remove').show();
} else {
image_section
.find(".sidebar-image")
@ -51,6 +54,8 @@ frappe.ui.form.set_user_image = function(frm) {
.css({'background-color': frappe.get_palette(title)})
.html(frappe.get_abbr(title));
image_actions.find('.sidebar-image-change').show();
image_actions.find('.sidebar-image-remove').hide();
}
}
@ -63,12 +68,27 @@ frappe.ui.form.setup_user_image_event = function(frm) {
});
}
// bind click on image_wrapper
frm.sidebar.image_wrapper.on('click', function() {
var field = frm.get_field(frm.meta.image_field);
if(!field.$input) {
field.make_input();
frm.sidebar.image_wrapper.on('click', ':not(.sidebar-image-actions)', (e) => {
let $target = $(e.currentTarget);
if ($target.is('a.dropdown-toggle, .dropdown')) {
return;
}
let dropdown = frm.sidebar.image_wrapper.find('.sidebar-image-actions .dropdown');
dropdown.toggleClass('open');
e.stopPropagation();
});
// bind click on image_wrapper
frm.sidebar.image_wrapper.on('click', '.sidebar-image-change, .sidebar-image-remove', function(e) {
let $target = $(e.currentTarget);
var field = frm.get_field(frm.meta.image_field);
if ($target.is('.sidebar-image-change')) {
if(!field.$input) {
field.make_input();
}
field.$input.trigger('click');
} else {
field.set_value('').then(() => frm.save());
}
field.$input.trigger('click');
});
}

View file

@ -12,6 +12,15 @@
<div class="sidebar-standard-image">
<div class="standard-image"></div>
</div>
<div class="sidebar-image-actions">
<div class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ __("Change") }}</a>
<ul class="dropdown-menu" role="menu">
<li><a class="sidebar-image-change">{{ __("Upload") }}</a></li>
<li><a class="sidebar-image-remove">{{ __("Remove") }}</a></li>
</ul>
</div>
</div>
</li>
</ul>
{% if frm.meta.beta %}
@ -72,6 +81,9 @@
<li class="h6 viewers-label">{%= __("Currently Viewing") %}</li>
<li class="form-viewers"></li>
</ul>
<ul class="list-unstyled sidebar-menu">
<a><li class="auto-repeat-status"><li></a>
</ul>
<ul class="list-unstyled sidebar-menu">
<li class="liked-by-parent">
<span class="liked-by">
@ -102,4 +114,4 @@
<ul class="list-unstyled visible-xs visible-sm">
<li class="close-sidebar">Close</li>
</ul>
</ul>

View file

@ -129,7 +129,10 @@ frappe.ui.form.Toolbar = Class.extend({
if(frappe.model.can_email(null, me.frm) && me.frm.doc.docstatus < 2) {
this.page.add_menu_item(__("Email"), function() {
me.frm.email_doc();
}, true, 'Ctrl+E');
}, true, {
shortcut: 'Ctrl+E',
condition: () => !this.frm.is_new()
});
}
// go to field modal
@ -168,7 +171,10 @@ frappe.ui.form.Toolbar = Class.extend({
&& frappe.model.can_delete(me.frm.doctype)) {
this.page.add_menu_item(__("Delete"), function() {
me.frm.savetrash();
}, true, 'Shift+Ctrl+D');
}, true, {
shortcut: 'Shift+Ctrl+D',
condition: () => !this.frm.is_new()
});
}
if(frappe.user_roles.includes("System Manager") && me.frm.meta.issingle === 0) {
@ -178,7 +184,7 @@ frappe.ui.form.Toolbar = Class.extend({
})
}, true);
if (frappe.boot.developer_mode===1 && me.frm.meta.issingle) {
if (frappe.boot.developer_mode===1) {
// edit doctype
this.page.add_menu_item(__("Edit DocType"), function() {
frappe.set_route('Form', 'DocType', me.frm.doctype);
@ -186,11 +192,21 @@ frappe.ui.form.Toolbar = Class.extend({
}
}
// Auto Repeat
if(this.can_repeat()) {
this.page.add_menu_item(__("Repeat"), function(){
frappe.utils.new_auto_repeat_prompt(me.frm);
}, true);
}
// New
if(p[CREATE] && !this.frm.meta.issingle) {
this.page.add_menu_item(__("New {0}", [__(me.frm.doctype)]), function() {
frappe.new_doc(me.frm.doctype, true);
}, true, 'Ctrl+B');
}, true, {
shortcut: 'Ctrl+B',
condition: () => !this.frm.is_new()
});
}
// Navigate
@ -203,6 +219,11 @@ frappe.ui.form.Toolbar = Class.extend({
});
}
},
can_repeat: function() {
return this.frm.meta.allow_auto_repeat
&& !this.frm.is_new()
&& !this.frm.doc.auto_repeat;
},
can_save: function() {
return this.get_docstatus()===0;
},

View file

@ -49,7 +49,6 @@ frappe.ui.form.States = Class.extend({
},
refresh: function() {
const me = this;
// hide if its not yet saved
if(this.frm.doc.__islocal) {
this.set_default_state();
@ -59,8 +58,6 @@ frappe.ui.form.States = Class.extend({
// state text
const state = this.get_state();
let doctype = this.frm.doctype;
if(state) {
// show actions from that state
this.show_actions(state);
@ -71,8 +68,6 @@ frappe.ui.form.States = Class.extend({
var added = false;
var me = this;
this.frm.page.clear_actions_menu();
// if the loaded doc is dirty, don't show workflow buttons
if (this.frm.doc.__unsaved===1) {
return;
@ -90,7 +85,8 @@ frappe.ui.form.States = Class.extend({
}
frappe.workflow.get_transitions(this.frm.doc).then(transitions => {
$.each(transitions, function(i, d) {
this.frm.page.clear_actions_menu();
transitions.forEach(d => {
if(frappe.user_roles.includes(d.allowed) && has_approval_access(d)) {
added = true;
me.frm.page.add_action_item(__(d.action), function() {

View file

@ -52,21 +52,31 @@
</ul>
</div>
</li>
<li class="assigned-to" style="display: none">
<a class = "dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" href="#" onclick="return false;">
{%= __("Assigned To") %} <span class="caret"></span>
</a>
<ul class="dropdown-menu assigned-dropdown" style="max-height: 300px; overflow-y: auto;" role="menu">
<li><div class="list-loading text-center assigned-loading text-muted">
{%= (__("Loading") + "..." ) %}
</div>
</li>
</ul>
</li>
{% if(frappe.help.has_help(doctype)) { %}
<li><a class="help-link list-link" data-doctype="{{ doctype }}">{{ __("Help") }}</a></li>
{% } %}
</ul>
<ul class="list-unstyled sidebar-menu list-group-by">
</ul>
<ul class="list-unstyled sidebar-menu sidebar-stat">
<li class="list-sidebar-label stat-label">{{ __("Tags") }}</li>
<li class="list-stats list-link">
<div class="btn-group">
<a class = "dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" href="#" onclick="return false;">
{{ __("Tags") }}<span class="caret"></span>
</a>
<ul class="dropdown-menu list-stats-dropdown" role="menu">
<div class="dropdown-search">
<input type="text" placeholder="Search" class="form-control dropdown-search-input input-xs">
</div>
</ul>
</div>
</li>
<li class="list-link" style="margin-top: 10px;">
<a class="list-tag-preview hidden-xs text-muted">{{ __("Show tags") }}</a>
</li>
</ul>
<ul class="list-unstyled sidebar-menu charts-menu hide">
<li class="h6">{%= __("Charts") %}</li>
<li class="list-link">

View file

@ -13,7 +13,6 @@ frappe.views.ListSidebar = class ListSidebar {
constructor(opts) {
$.extend(this, opts);
this.make();
this.get_stats();
this.cat_tags = [];
}
@ -23,7 +22,7 @@ frappe.views.ListSidebar = class ListSidebar {
this.sidebar = $('<div class="list-sidebar overlay-sidebar hidden-xs hidden-sm"></div>')
.html(sidebar_content)
.appendTo(this.page.sidebar.empty());
this.setup_reports();
this.setup_list_filter();
this.setup_views();
@ -31,6 +30,7 @@ frappe.views.ListSidebar = class ListSidebar {
this.setup_calendar_view();
this.setup_email_inbox();
this.setup_keyboard_shortcuts();
this.setup_list_group_by();
let limits = frappe.boot.limits;
@ -38,13 +38,14 @@ frappe.views.ListSidebar = class ListSidebar {
this.setup_upgrade_box();
}
if(this.doctype !== 'ToDo') {
$('.assigned-to').show();
if (this.list_view.list_view_settings && this.list_view.list_view_settings.disable_sidebar_stats) {
this.sidebar.find('.sidebar-stat').remove();
} else {
this.sidebar.find('.list-stats').on('click', (e) => {
$(e.currentTarget).find('.stat-link').remove();
this.get_stats();
});
}
$('.assigned-to').on('click', () => {
$('.assigned').remove();
this.setup_assigned_to();
});
}
@ -225,31 +226,6 @@ frappe.views.ListSidebar = class ListSidebar {
});
}
setup_assigned_to() {
$('.assigned-loading').show();
let dropdown = this.page.sidebar.find('.assigned-dropdown');
let current_filters = this.list_view.get_filters_for_args();
frappe.call('frappe.desk.listview.get_user_assignments_and_count', {doctype: this.doctype, current_filters: current_filters}).then((data) => {
$('.assigned-loading').hide();
let current_user = data.message.find(user => user.name === frappe.session.user);
if(current_user) {
let current_user_count = current_user.count;
this.get_html_for_assigned(frappe.session.user, current_user_count).appendTo(dropdown);
}
let user_list = data.message.filter(user => !['Guest', frappe.session.user, 'Administrator'].includes(user.name) && user.count!==0 );
user_list.forEach((user) => {
this.get_html_for_assigned(user.name, user.count).appendTo(dropdown);
});
$(".assigned-dropdown li a").on("click", (e) => {
let assigned_user = $(e.currentTarget).find($('.assigned-user')).text();
if(assigned_user === 'Me') assigned_user = frappe.session.user;
this.list_view.filter_area.remove('_assign');
this.list_view.filter_area.add(this.list_view.doctype, "_assign", "like", `%${assigned_user}%`);
});
});
}
setup_keyboard_shortcuts() {
this.sidebar.find('.list-link > a, .list-link > .btn-group > a').each((i, el) => {
frappe.ui.keys
@ -258,12 +234,38 @@ frappe.views.ListSidebar = class ListSidebar {
});
}
get_html_for_assigned(name, count) {
if (name === frappe.session.user) name='Me';
if (count > 99) count='99+';
let html = $('<li class="assigned"><a class="badge-hover" href="#" onclick="return false;" role="assigned-item"><span class="assigned-user">'
+ name + '</span><span class="badge pull-right" style="position:relative">' + count + '</span></a></li>');
return html;
setup_list_group_by() {
this.list_group_by = new frappe.views.ListGroupBy({
doctype: this.doctype,
sidebar: this,
list_view: this.list_view,
page: this.page
});
}
setup_dropdown_search(dropdown, text_class) {
let $dropdown_search = dropdown.find('.dropdown-search').show();
let $search_input = $dropdown_search.find('.dropdown-search-input');
$search_input.focus();
$dropdown_search.on('click',(e)=>{
e.stopPropagation();
});
let $elements = dropdown.find('li');
$dropdown_search.on('keyup',()=> {
let text_filter = $search_input.val().toLowerCase();
let text;
for (var i = 0; i < $elements.length; i++) {
text = $elements.eq(i).find(text_class).text();
if (text.toLowerCase().indexOf(text_filter) > -1) {
$elements.eq(i).css('display','');
} else {
$elements.eq(i).css('display','none');
}
}
});
dropdown.parent().on('hide.bs.dropdown',()=> {
$dropdown_search.val('');
});
}
setup_upgrade_box() {
@ -302,9 +304,6 @@ frappe.views.ListSidebar = class ListSidebar {
get_stats() {
var me = this;
if (this.list_view.list_view_settings && this.list_view.list_view_settings.disable_sidebar_stats) {
return;
}
frappe.call({
method: 'frappe.desk.reportview.get_sidebar_stats',
type: 'GET',
@ -337,6 +336,8 @@ frappe.views.ListSidebar = class ListSidebar {
//render normal stats
me.render_stat("_user_tags", (r.message.stats || {})["_user_tags"]);
}
let stats_dropdown = me.sidebar.find('.list-stats-dropdown');
me.setup_dropdown_search(stats_dropdown,'.stat-label');
}
});
}
@ -399,7 +400,7 @@ frappe.views.ListSidebar = class ListSidebar {
me.list_view.refresh();
});
})
.insertBefore(this.sidebar.find(".close-sidebar-button"));
.appendTo(this.sidebar.find(".list-stats-dropdown"));
}
set_fieldtype(df) {

View file

@ -0,0 +1,175 @@
frappe.provide('frappe.views');
frappe.views.ListGroupBy = class ListGroupBy {
constructor(opts) {
$.extend(this, opts);
this.make_wrapper();
this.user_settings = frappe.get_user_settings(this.doctype);
this.group_by_fields = ['assigned_to'];
if(this.user_settings.group_by_fields) {
this.group_by_fields = this.group_by_fields.concat(this.user_settings.group_by_fields);
}
this.render_group_by_items();
this.make_group_by_fields_modal();
this.setup_dropdown();
this.setup_filter_by();
}
make_group_by_fields_modal() {
let d = new frappe.ui.Dialog ({
title: __("Add Filter By"),
fields: this.get_group_by_dropdown_fields()
});
d.set_primary_action("Add", ({ group_by_fields }) => {
frappe.model.user_settings.save(this.doctype, 'group_by_fields', group_by_fields || null);
this.group_by_fields = group_by_fields ? ['assigned_to', ...group_by_fields] : ['assigned_to'];
this.render_group_by_items();
d.hide();
});
this.page.sidebar.find(".add-list-group-by a ").on("click", () => {
d.show();
});
}
make_wrapper() {
this.$wrapper = this.sidebar.sidebar.find('.list-group-by');
let html = `
<li class="list-sidebar-label">
${__('Filter By')}
</li>
<div class="list-group-by-fields">
</div>
<li class="add-list-group-by list-link">
<a class="add-group-by hidden-xs text-muted">
${__("Add Fields")} <i class="octicon octicon-plus" style="margin-left: 2px;"></i>
</a>
</li>
`;
this.$wrapper.html(html);
}
render_group_by_items() {
let get_item_html = (fieldname) => {
let label = fieldname === 'assigned_to'
? __('Assigned To')
: frappe.meta.get_label(this.doctype, fieldname);
return `<li class="group-by-field list-link">
<div class="btn-group">
<a class = "dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
data-label="${label}" data-fieldname="${fieldname}" href="#" onclick="return false;">
${__(label)}<span class="caret"></span>
</a>
<ul class="dropdown-menu group-by-dropdown" role="menu">
<li><div class="list-loading text-center group-by-loading text-muted">
${__("Loading...")}
</div>
</li>
</ul>
</div>
</li>`;
};
let html = this.group_by_fields.map(get_item_html).join('');
this.$wrapper.find('.list-group-by-fields').html(html);
}
setup_dropdown() {
this.$wrapper.on('click', '.group-by-field', (e)=> {
let dropdown = $(e.currentTarget).find('.group-by-dropdown');
let fieldname = $(e.currentTarget).find('a').attr('data-fieldname');
this.get_group_by_count(fieldname).then(field_count_list => {
if (field_count_list.length) {
this.render_dropdown_items(field_count_list, dropdown);
this.sidebar.setup_dropdown_search(dropdown, '.group-by-value');
} else {
dropdown.find('.group-by-loading').hide();
}
});
});
}
get_group_by_dropdown_fields() {
let group_by_fields = [];
let fields = this.list_view.meta.fields.filter((f)=> ["Select", "Link"].includes(f.fieldtype));
group_by_fields.push({
label: __(this.doctype),
fieldname: 'group_by_fields',
fieldtype: 'MultiCheck',
columns: 2,
options: fields
.map(df => ({
label: __(df.label),
value: df.fieldname,
checked: this.group_by_fields.includes(df.fieldname)
}))
});
return group_by_fields;
}
get_group_by_count(field) {
let args = {
doctype: this.doctype,
current_filters: this.list_view.get_filters_for_args(),
field: field,
};
return frappe.call('frappe.desk.listview.get_group_by_count', args).then((r) => {
let field_counts = r.message || [];
field_counts = field_counts.filter(f => f.count !== 0);
if (field === 'assigned_to') {
field_counts = field_counts.filter(f => !['Guest', 'Administrator'].includes(f.name));
}
return field_counts;
});
}
render_dropdown_items(fields, dropdown) {
let get_dropdown_html = (field) => {
let label = field.name == null ? __('Not Specified') : field.name;
if (label === frappe.session.user) {
label = __('Me');
}
let value = field.name == null ? '' : encodeURIComponent(field.name);
return `<li class="group-by-item" data-value="${value}">
<a class="badge-hover" href="#" onclick="return false;">
<span class="group-by-value">${label}</span>
<span class="badge pull-right group-by-count">${field.count}</span>
</a>
</li>`;
};
let standard_html = `
<div class="dropdown-search">
<input type="text" placeholder="${__('Search')}" class="form-control dropdown-search-input input-xs">
</div>
`;
let dropdown_html = standard_html + fields.map(get_dropdown_html).join('');
dropdown.html(dropdown_html);
}
setup_filter_by() {
this.$wrapper.on('click', '.group-by-item', (e) => {
let $target = $(e.currentTarget);
let fieldname = $target.parents('.group-by-field').find('a').data('fieldname');
let value = decodeURIComponent($target.data('value').trim());
fieldname = fieldname === 'assigned_to' ? '_assign': fieldname;
this.list_view.filter_area.remove(fieldname);
let operator = '=';
if (value === '') {
operator = 'is';
value = 'not set';
}
if (fieldname === '_assign') {
operator = 'like';
value = `%${value}%`;
}
this.list_view.filter_area.add(this.doctype, fieldname, operator, value);
});
}
};

View file

@ -1,22 +1,16 @@
<ul class="list-unstyled sidebar-menu sidebar-stat">
<li class="divider"></li>
<li class="h6 stat-label">{{ label }}</li>
{% if(!stat.length) { %}
<li class="stat-no-records text-muted">{{ __("No records tagged.") }}</li>
{% } else {
for (var i=0, l=stat.length; i < l; i++) {
var stat_label = stat[i][0];
var stat_count = stat[i][1];
%}
<li>
<a class="stat-link badge-hover" data-label="{{ stat_label %}" data-field="{{ field %}">
<span class="badge">{{ stat_count }}</span>
<span>{{ __(stat_label) }}</span>
</a>
</li>
{% }
} %}
</ul>
<div style="margin-top: 10px;">
<a class="list-tag-preview hidden-xs text-muted">{{ __("Show tags") }}</a>
</div>
{% if(!stat.length) { %}
<li class="stat-no-records text-muted">{{ __("No records tagged.") }}</li>
{% } else {
for (var i=0, l=stat.length; i < l; i++) {
var stat_label = stat[i][0];
var stat_count = stat[i][1];
%}
<li>
<a class="stat-link badge-hover" data-label="{{ stat_label %}" data-field="{{ field %}" href="#" onclick="return false;">
<span class="badge pull-right" style="position: relative">{{ stat_count }}</span>
<span class="stat-label">{{ __(stat_label) }}</span>
</a>
</li>
{% }
} %}

View file

@ -655,7 +655,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
get_count_str() {
const current_count = this.data.length;
let current_count = this.data.length;
let count_without_children = this.data.uniqBy(d => d.name).length;
const filters = this.get_filters_for_args();
const with_child_table_filter = filters.some(filter => {
return filter[0] !== this.doctype;
@ -676,7 +678,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
}).then(r => {
this.total_count = r.message.values[0][0] || current_count;
const str = __('{0} of {1}', [current_count, this.total_count]);
let str = __('{0} of {1}', [current_count, this.total_count]);
if (count_without_children !== current_count) {
str = __('{0} of {1} ({2} rows with children)', [count_without_children, this.total_count, current_count]);
}
return str;
});
}

View file

@ -80,6 +80,11 @@ frappe.call = function(opts) {
let url = opts.url;
if (!url) {
url = '/api/method/' + args.cmd;
if (window.cordova) {
let host = frappe.request.url;
host = host.slice(0, host.length - 1);
url = host + url;
}
delete args.cmd;
}

View file

@ -42,6 +42,10 @@ frappe.ui.keys.bind_shortcut_group_event = () => {
highlight_alt_shortcuts();
}
if (e.shiftKey || e.ctrlKey || e.metaKey) {
return;
}
if (key && e.altKey) {
let shortcut = get_shortcut_for_key(key);
if (shortcut) {

View file

@ -21,17 +21,21 @@ frappe.ui.keys.setup = function() {
let standard_shortcuts = [];
frappe.ui.keys.standard_shortcuts = standard_shortcuts;
frappe.ui.keys.add_shortcut = ({shortcut, action, description, page, target, ignore_inputs = false} = {}) => {
frappe.ui.keys.add_shortcut = ({shortcut, action, description, page, target, condition, ignore_inputs = false} = {}) => {
if (target instanceof jQuery) {
let $target = target;
action = () => {
$target[0].click();
}
}
frappe.ui.keys.on(shortcut, (e) => {
if (!condition) {
condition = () => true;
}
let handler = (e) => {
let $focused_element = $(document.activeElement);
let is_input_focused = $focused_element.is('input, select, textarea, [contenteditable=true]');
if (is_input_focused && !ignore_inputs) return;
if (!condition()) return;
if (!page || page.wrapper.is(':visible')) {
let prevent_default = action(e);
@ -41,11 +45,19 @@ frappe.ui.keys.add_shortcut = ({shortcut, action, description, page, target, ign
e.preventDefault();
}
}
});
};
// monkey patch page to handler
handler.page = page;
// remove handler with the same page attached to it
frappe.ui.keys.off(shortcut, page);
// attach new handler
frappe.ui.keys.on(shortcut, handler);
// update standard shortcut list
let existing_shortcut_index = standard_shortcuts.findIndex(
s => s.shortcut === shortcut
);
let new_shortcut = { shortcut, action, description, page };
let new_shortcut = { shortcut, action, description, page, condition };
if (existing_shortcut_index === -1) {
standard_shortcuts.push(new_shortcut);
} else {
@ -54,6 +66,8 @@ frappe.ui.keys.add_shortcut = ({shortcut, action, description, page, target, ign
}
frappe.ui.keys.show_keyboard_shortcut_dialog = () => {
if (frappe.ui.keys.is_dialog_shown) return;
let global_shortcuts = standard_shortcuts.filter(shortcut => !shortcut.page);
let current_page_shortcuts = standard_shortcuts.filter(
shortcut => shortcut.page && shortcut.page === window.cur_page.page.page);
@ -62,19 +76,21 @@ frappe.ui.keys.show_keyboard_shortcut_dialog = () => {
if (!shortcuts.length) {
return '';
}
let html = shortcuts.map(shortcut => {
let shortcut_label = shortcut.shortcut
.split('+')
.map(frappe.utils.to_title_case)
.join('+');
if (frappe.utils.is_mac()) {
shortcut_label = shortcut_label.replace('Ctrl', '⌘');
}
return `<tr>
<td width="40%"><kbd>${shortcut_label}</kbd></td>
<td width="60%">${shortcut.description || ''}</td>
</tr>`;
}).join('');
let html = shortcuts
.filter(s => s.condition ? s.condition() : true)
.map(shortcut => {
let shortcut_label = shortcut.shortcut
.split('+')
.map(frappe.utils.to_title_case)
.join('+');
if (frappe.utils.is_mac()) {
shortcut_label = shortcut_label.replace('Ctrl', '⌘');
}
return `<tr>
<td width="40%"><kbd>${shortcut_label}</kbd></td>
<td width="60%">${shortcut.description || ''}</td>
</tr>`;
}).join('');
html = `<h5 style="margin: 0;">${heading}</h5>
<table style="margin-top: 10px;" class="table table-bordered">
${html}
@ -87,6 +103,9 @@ frappe.ui.keys.show_keyboard_shortcut_dialog = () => {
let dialog = new frappe.ui.Dialog({
title: __('Keyboard Shortcuts'),
on_hide() {
frappe.ui.keys.is_dialog_shown = false;
}
});
dialog.$body.append(global_shortcuts_html);
@ -98,6 +117,7 @@ frappe.ui.keys.show_keyboard_shortcut_dialog = () => {
`);
dialog.show();
frappe.ui.keys.is_dialog_shown = true;
}
frappe.ui.keys.get_key = function(e) {
@ -130,6 +150,15 @@ frappe.ui.keys.on = function(key, handler) {
frappe.ui.keys.handlers[key].push(handler);
}
frappe.ui.keys.off = function(key, page) {
let handlers = frappe.ui.keys.handlers[key];
if (!handlers || handlers.length === 0) return;
frappe.ui.keys.handlers[key] = handlers.filter(h => {
if (!page) return false;
return h.page !== page;
});
}
frappe.ui.keys.add_shortcut({
shortcut: 'ctrl+s',
action: function(e) {
@ -157,7 +186,7 @@ frappe.ui.keys.add_shortcut({
e.preventDefault();
$('.navbar-home img').click();
},
description: __('Home')
description: __('Navigate Home')
});
frappe.ui.keys.add_shortcut({
@ -166,7 +195,7 @@ frappe.ui.keys.add_shortcut({
e.preventDefault();
$('.dropdown-navbar-user a').eq(0).click();
},
description: __('Settings')
description: __('Open Settings')
});
frappe.ui.keys.add_shortcut({
@ -174,7 +203,7 @@ frappe.ui.keys.add_shortcut({
action: function() {
frappe.ui.keys.show_keyboard_shortcut_dialog();
},
description: __('Keyboard Shortcuts')
description: __('Show Keyboard Shortcuts')
});
frappe.ui.keys.add_shortcut({
@ -183,7 +212,7 @@ frappe.ui.keys.add_shortcut({
e.preventDefault();
$('.dropdown-help a').eq(0).click();
},
description: __('Help')
description: __('Open Help')
});
frappe.ui.keys.on('escape', function(e) {

View file

@ -1,7 +1,7 @@
frappe.ui.LinkPreview = class {
constructor() {
this.$links = [];
this.popovers_list = [];
this.LINK_CLASSES = 'a[data-doctype], input[data-fieldtype="Link"], .popover';
this.popover_timeout = null;
this.setup_events();
@ -104,7 +104,7 @@ frappe.ui.LinkPreview = class {
}
handle_popover_hide() {
$(document.body).on('mouseout', this.LINK_CLASSES, () => {
$(document).on('mouseout', this.LINK_CLASSES, () => {
// To allow popover to be hovered on
if (!$('.popover:hover').length) {
this.link_hovered = false;
@ -129,7 +129,7 @@ frappe.ui.LinkPreview = class {
}
clear_all_popovers() {
this.$links.forEach($el => $el.popover('hide'));
this.popovers_list.forEach($el => $el.hide());
}
get_preview_fields() {
@ -190,7 +190,7 @@ frappe.ui.LinkPreview = class {
$popover.addClass('link-preview-popover');
$popover.toggleClass('control-field-popover', this.is_link);
this.$links.push(this.element);
this.popovers_list.push(this.element.data('bs.popover'));
}

View file

@ -313,21 +313,12 @@ frappe.ui.Page = Class.extend({
let $li;
if (shortcut) {
let shortcut_label = shortcut;
if (frappe.utils.is_mac()) {
shortcut_label = shortcut.replace('Ctrl', '⌘');
}
let shortcut_obj = this.prepare_shortcut_obj(shortcut, click, label);
$li = $(`<li><a class="grey-link dropdown-item" href="#" onClick="return false;">
<span class="menu-item-label">${label}</span>
<span class="text-muted pull-right">${shortcut_label}</span>
<span class="text-muted pull-right">${shortcut_obj.shortcut_label}</span>
</a><li>`);
shortcut = shortcut.toLowerCase();
frappe.ui.keys.add_shortcut({
shortcut,
target: $li.find('a'),
description: label,
page: this
});
frappe.ui.keys.add_shortcut(shortcut_obj);
} else {
$li = $(`<li><a class="grey-link dropdown-item" href="#" onClick="return false;">
<span class="menu-item-label">${label}</span></a><li>`);
@ -354,6 +345,35 @@ frappe.ui.Page = Class.extend({
return $link;
},
prepare_shortcut_obj(shortcut, click, label) {
let shortcut_obj;
// convert to object, if shortcut string passed
if (typeof shortcut === 'string') {
shortcut_obj = { shortcut };
} else {
shortcut_obj = shortcut;
}
// label
if (frappe.utils.is_mac()) {
shortcut_obj.shortcut_label = shortcut_obj.shortcut.replace('Ctrl', '⌘');
} else {
shortcut_obj.shortcut_label = shortcut_obj.shortcut;
}
// actual shortcut string
shortcut_obj.shortcut = shortcut_obj.shortcut.toLowerCase();
// action is button click
if (!shortcut_obj.action) {
shortcut_obj.action = click;
}
// shortcut description can be button label
if (!shortcut_obj.description) {
shortcut_obj.description = label;
}
// page
shortcut_obj.page = this;
return shortcut_obj;
},
/*
* Check if there already exists a button with a specified label in a specified button group
* @param {object} parent - This should be the `ul` of the button group.

View file

@ -26,6 +26,8 @@ frappe.ui.misc.about = function() {
show_versions(r.message);
}
})
} else {
show_versions(frappe.versions);
}
};

View file

@ -258,4 +258,59 @@ frappe.utils.xss_sanitise = function (string, options) {
}
return sanitised;
}
}
frappe.utils.new_auto_repeat_prompt = function(frm) {
const fields = [
{
'fieldname': 'frequency',
'fieldtype': 'Select',
'label': __('Frequency'),
'reqd': 1,
'options': [
{'label': __('Daily'), 'value': 'Daily'},
{'label': __('Weekly'), 'value': 'Weekly'},
{'label': __('Monthly'), 'value': 'Monthly'},
{'label': __('Quarterly'), 'value': 'Quarterly'},
{'label': __('Half-yearly'), 'value': 'Half-yearly'},
{'label': __('Yearly'), 'value': 'Yearly'}
]
},
{
'fieldname': 'start_date',
'fieldtype': 'Date',
'label': __('Start Date'),
'reqd': 1,
'default': frappe.datetime.nowdate()
},
{
'fieldname': 'end_date',
'fieldtype': 'Date',
'label': __('End Date')
}
];
frappe.prompt(fields, function(values) {
frappe.call({
method: "frappe.automation.doctype.auto_repeat.auto_repeat.make_auto_repeat",
args: {
'doctype': frm.doc.doctype,
'docname': frm.doc.name,
'frequency': values['frequency'],
'start_date': values['start_date'],
'end_date': values['end_date']
},
callback: function (r) {
if (r.message) {
frappe.show_alert({
'message': __("Auto Repeat created for this document"),
'indicator': 'green'
});
frm.reload_doc();
}
}
});
},
__('Auto Repeat'),
__('Save')
);
}

View file

@ -11,6 +11,16 @@ frappe.breadcrumbs = {
"Dashboard Chart Source": "Customization",
},
module_map: {
'Core': 'Settings',
'Email': 'Settings',
'Custom': 'Settings',
'Workflow': 'Settings',
'Printing': 'Settings',
'Automation': 'Settings',
'Setup': 'Settings',
},
set_doctype_module: function(doctype, module) {
localStorage["preferred_breadcrumbs:" + doctype] = module;
},
@ -73,8 +83,8 @@ frappe.breadcrumbs = {
}
if(breadcrumbs.module) {
if(in_list(["Core", "Email", "Custom", "Workflow", "Print"], breadcrumbs.module)) {
breadcrumbs.module = "Setup";
if (frappe.breadcrumbs.module_map[breadcrumbs.module]) {
breadcrumbs.module = frappe.breadcrumbs.module_map[breadcrumbs.module];
}
if(frappe.get_module(breadcrumbs.module)) {

View file

@ -3,6 +3,7 @@
v-if="!hidden"
class="border module-box"
:class="{ 'hovered-box': hovered }"
:data-module-name="module_name"
>
<div class="flush-top">
<div class="module-box-content">
@ -10,7 +11,7 @@
<a class="module-box-link" :href="type === 'module' ? '#modules/' + module_name : link">
<h4 class="h4">
<div>
<i :class="iconClass" style="color:#8d99a6;font-size:18px;margin-right:6px;"></i>
<i :class="icon_class" style="color:#8d99a6;font-size:18px;margin-right:6px;"></i>
{{ label }}
</div>
</h4>
@ -54,7 +55,7 @@ export default {
};
},
computed: {
iconClass() {
icon_class() {
if (this.icon) {
return this.icon;
} else {
@ -82,7 +83,12 @@ export default {
background-color: #ffffff;
}
.module-box:hover {
.module-box.sortable-chosen {
background-color: @disabled-background;
border-color: @disabled-background;
}
.modules-container:not(.dragging) .module-box:hover {
border-color: @text-muted;
}

View file

@ -4,10 +4,10 @@
<div class="module-category h6 uppercase">{{ category }}</div>
</div>
<div class="modules-container">
<div class="modules-container" :class="{'dragging': dragging}" ref="modules-container">
<desk-module-box
v-for="(module, index) in modules"
:key="module.name"
:key="module.module_name"
:index="index"
v-bind="module"
@customize="show_module_card_customize_dialog(module)"
@ -24,7 +24,32 @@ export default {
components: {
DeskModuleBox
},
data() {
return {
dragging: false
}
},
mounted() {
this.setup_sortable();
},
methods: {
setup_sortable() {
let modules_container =this.$refs['modules-container'];
this.sortable = new Sortable(modules_container, {
animation: 150,
onStart: () => this.dragging = true,
onEnd: () => {
this.dragging = false;
let modules = Array.from(modules_container.querySelectorAll('.module-box'))
.map(node => node.dataset.moduleName);
this.$emit('module-order-change', {
module_category: this.category,
modules
});
}
})
},
show_module_card_customize_dialog(module) {
const d = new frappe.ui.Dialog({
title: __('Customize Shortcuts'),
@ -34,7 +59,7 @@ export default {
fieldname: 'links',
fieldtype: 'MultiSelectPills',
get_data() {
return frappe.call('frappe.desk.moduleview.get_links', {
return frappe.call('frappe.desk.moduleview.get_links_for_module', {
app: module.app,
module: module.module_name,
}).then(r => r.message);
@ -48,7 +73,7 @@ export default {
module_name: module.module_name,
links
}).then(r => {
this.$emit('update_home_settings', r.message);
this.$emit('update-desktop-settings', r.message);
});
d.hide();
}

View file

@ -14,7 +14,8 @@
v-if="get_modules_for_category(category).length"
:category="category"
:modules="get_modules_for_category(category)"
@update_home_settings="hs => update_modules_with_home_settings(hs)"
@update-desktop-settings="update_desktop_settings"
@module-order-change="update_module_order"
>
</desk-section>
</div>
@ -30,99 +31,96 @@ export default {
DeskSection
},
data() {
let modules_list = frappe.boot.allowed_modules
.filter(d => (d.type==='module' || d.category==='Places') && !d.blocked)
.map(d => {
d.links = (d.links || []).map(link => {
link.route = generate_route(link);
return link;
});
return d;
});
return {
module_categories: ['Modules', 'Domains', 'Places', 'Administration'],
modules: modules_list,
modules: [],
home_settings_fetched: false
};
},
created() {
this.fetch_home_settings();
this.fetch_desktop_settings();
},
methods: {
fetch_home_settings() {
return frappe.db.get_value('User', user, 'home_settings')
fetch_desktop_settings() {
frappe.call('frappe.desk.moduleview.get_desktop_settings')
.then(r => {
let home_settings = JSON.parse(r.message.home_settings || '{}');
this.update_modules_with_home_settings(home_settings);
this.home_settings_fetched = true;
if (r.message) {
this.update_desktop_settings(r.message);
this.home_settings_fetched = true;
}
});
},
update_modules_with_home_settings(home_settings) {
this.modules = this.modules.map(m => {
let hidden_modules = home_settings.hidden_modules || [];
m.hidden = hidden_modules.includes(m.module_name);
let links = home_settings.links && home_settings.links[m.module_name];
if (links) {
links = JSON.parse(links);
let default_links = m.links.map(link => link.name);
m.links = m.links.map(link => {
link.hidden = !links.includes(link.name);
update_desktop_settings(desktop_settings) {
this.modules = this.add_routes_for_module_links(desktop_settings);
},
add_routes_for_module_links(user_settings) {
for (let category in user_settings) {
user_settings[category] = user_settings[category].map(m => {
m.links = (m.links || []).map(link => {
link.route = generate_route(link);
return link;
});
let new_links = links
.filter(link => !default_links.includes(link))
.filter(Boolean)
.map(link => {
let new_link = { name: link, label: link, type: 'doctype' };
new_link.route = generate_route(new_link);
return new_link;
});
m.links = m.links.concat(new_links);
}
return m;
});
return m;
});
}
return user_settings;
},
update_module_order({ module_category, modules }) {
frappe.call('frappe.desk.moduleview.update_modules_order', { module_category, modules });
},
get_modules_for_category(category) {
return this.modules.filter(m => m.category === category && !m.hidden);
return this.modules[category] || [];
},
show_hide_cards_dialog() {
let fields = this.module_categories.map(category => {
let modules = this.modules.filter(m => m.category === category);
let options = modules.map(
m => ({ label: m.label, value: m.module_name, checked: !m.hidden })
);
return {
label: category,
fieldname: category,
fieldtype: 'MultiCheck',
options,
columns: 2
}
});
const d = new frappe.ui.Dialog({
title: __('Show / Hide Cards'),
fields: fields.filter(f => f.options.length > 0),
primary_action_label: __('Save'),
primary_action: (values) => {
let all_modules = this.modules.map(m => m.module_name);
let modules_to_show = Object.keys(values).map(k => values[k]).flatMap(m => m);
let modules_to_hide = all_modules.filter(m => !modules_to_show.includes(m));
d.hide();
frappe.call('frappe.desk.moduleview.get_options_for_show_hide_cards')
.then(r => {
let module_options = r.message;
let fields = this.module_categories.map(category => {
let options = module_options.filter(m => m.category === category);
return {
label: category,
fieldname: category,
fieldtype: 'MultiCheck',
options,
columns: 2
}
}).filter(f => f.options.length > 0);
frappe.call('frappe.desk.moduleview.hide_modules_from_desktop', {
modules: modules_to_hide
})
.then(r => r.message)
.then(hs => this.update_modules_with_home_settings(hs));
}
});
let old_values = null;
d.show();
const d = new frappe.ui.Dialog({
title: __('Show / Hide Cards'),
fields: fields,
primary_action_label: __('Save'),
primary_action: (values) => {
let category_map = {};
for (let category of this.module_categories) {
let old_modules = old_values[category] || [];
let new_modules = values[category] || [];
let removed = old_modules.filter(module => !new_modules.includes(module));
let added = new_modules.filter(module => !old_modules.includes(module));
category_map[category] = { added, removed };
}
frappe.call({
method: 'frappe.desk.moduleview.update_hidden_modules',
args: { category_map },
btn: d.get_primary_btn()
}).then(r => {
this.update_desktop_settings(r.message)
d.hide();
});
}
});
d.show();
// deepcopy
old_values = JSON.parse(JSON.stringify(d.get_values()));
});
}
}
}

View file

@ -13,10 +13,15 @@
<tr>
{% for col in columns %}
{% if col.name && col._id !== "_check" %}
<th style="min-width: {{ col.minWidth }}px"
<th
{% if col.minWidth %}
style="min-width: {{ col.minWidth }}px"
{% endif %}
{% if col.docfield && frappe.model.is_numeric_field(col.docfield) %}
class="text-right"
{% endif %}>{{ __(col.name) }}</th>
{% endif %}
>
{{ __(col.name) }}</th>
{% endif %}
{% endfor %}
</tr>

View file

@ -493,6 +493,15 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
if (!(options && options.data && options.data.labels && options.data.labels.length > 0)) return;
if (options.fieldtype) {
options.tooltipOptions = {
formatTooltipY: d => frappe.format(d, {
fieldtype: options.fieldtype,
options: options.options
})
};
}
return options;
}
@ -889,11 +898,16 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
options: ['Excel', 'CSV'],
default: 'Excel',
reqd: 1
},
{
label: __("Include indentation"),
fieldname: "include_indentation",
fieldtype: "Check",
}
], ({ file_format }) => {
], ({ file_format, include_indentation }) => {
if (file_format === 'CSV') {
const column_row = this.columns.map(col => col.label);
const data = this.get_data_for_csv();
const data = this.get_data_for_csv(include_indentation);
const out = [column_row].concat(data);
frappe.tools.downloadify(out, null, this.report_name);
@ -914,6 +928,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
file_format_type: file_format,
filters: filters,
visible_idx,
include_indentation,
};
open_url_post(frappe.request.url, args);
@ -921,7 +936,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}, __('Export Report: '+ this.report_name), __('Download'));
}
get_data_for_csv() {
get_data_for_csv(include_indentation) {
const indices = this.datatable.bodyRenderer.visibleRowIndices;
const rows = indices.map(i => this.datatable.datamanager.getRow(i));
return rows.map(row => {
@ -929,8 +944,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
return row
.slice(standard_column_count)
.map((cell, i) => {
if (i === 0) {
return ' '.repeat(row.meta.indent) + (cell.content || '');
if (include_indentation && i===0) {
cell.content = ' '.repeat(row.meta.indent) + (cell.content || '');
}
return cell.content || '';
});
@ -976,11 +991,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
{
label: __('Print'),
action: () => {
frappe.ui.get_print_settings(
let dialog = frappe.ui.get_print_settings(
false,
print_settings => this.print_report(print_settings),
this.report_doc.letter_head
);
this.add_portrait_warning(dialog);
},
condition: () => frappe.model.can_print(this.report_doc.ref_doctype),
standard: true
@ -988,11 +1004,13 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
{
label: __('PDF'),
action: () => {
frappe.ui.get_print_settings(
let dialog = frappe.ui.get_print_settings(
false,
print_settings => this.pdf_report(print_settings),
this.report_doc.letter_head
);
this.add_portrait_warning(dialog);
},
condition: () => frappe.model.can_print(this.report_doc.ref_doctype),
standard: true
@ -1125,6 +1143,18 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
];
}
add_portrait_warning(dialog) {
if (this.columns.length > 10) {
dialog.set_df_property('orientation', 'change', () => {
let value = dialog.get_value('orientation');
let description = value === 'Portrait'
? __('Report with more than 10 columns looks better in Landscape mode.')
: '';
dialog.set_df_property('orientation', 'description', description);
});
}
}
add_custom_column(custom_column, custom_data, link_field, column_field, insert_after) {
const column = this.prepare_columns(custom_column);

View file

@ -62,7 +62,7 @@ frappe.views.TreeView = Class.extend({
this.page = this.parent.page;
frappe.container.change_to(this.page_name);
frappe.breadcrumbs.add(me.opts.breadcrumb || locals.DocType[me.doctype].module);
frappe.breadcrumbs.add(me.opts.breadcrumb || locals.DocType[me.doctype].module, me.doctype);
this.set_title();

View file

@ -44,6 +44,12 @@ export default class WebForm extends frappe.ui.FieldGroup {
else return;
}
set_default_values() {
let values = frappe.utils.get_query_params();
delete values.new;
this.set_values(values);
}
set_form_description(intro) {
let intro_wrapper = document.getElementById('introduction');
intro_wrapper.innerHTML = intro;

View file

@ -59,6 +59,7 @@ frappe.ready(function() {
web_form.prepare(web_form_doc, r.message.doc || {});
web_form.make();
web_form.set_default_values();
})
function get_data() {

View file

@ -722,7 +722,9 @@ _f.Frm.prototype._save = function(save_action, callback, btn, on_error, resolve,
me.script_manager.trigger("after_save");
// submit comment if entered
me.timeline.comment_area.submit();
if (me.timeline) {
me.timeline.comment_area.submit();
}
me.refresh();
} else {
if (on_error) {

View file

@ -104,6 +104,7 @@
.form-inner-toolbar .dropdown-menu {
right: 0px;
left: auto;
z-index: 100;
}
.layout-main-section {

View file

@ -126,9 +126,6 @@ body[data-route^="Module"] .main-menu {
}
}
.stat-link {
margin-bottom: 0.5em;
}
a.close {
position: absolute;
@ -188,19 +185,32 @@ body[data-route^="Module"] .main-menu {
border-radius: 6px;
}
.sidebar-image-wrapper:after {
content: '\A';
position: absolute;
width: 100%; height:100%;
top:0; left:0;
background: #fff;
opacity: 0;
transition: all 0.5s;
-webkit-transition: all 0.6s;
.sidebar-image-wrapper {
position: relative;
}
.sidebar-image-wrapper:hover:after {
opacity: 0.5;
.sidebar-image, .sidebar-standard-image {
transition: opacity 0.3s;
}
.sidebar-image-wrapper:hover {
.sidebar-image, .sidebar-standard-image {
opacity: 0.5;
}
.sidebar-image-actions {
display: block;
}
}
.sidebar-image-actions {
display: none;
position: absolute;
top: 50%;
right: 0;
left: 0;
transform: translateY(-50%);
text-align: center;
z-index: 1;
}
}
@ -374,14 +384,25 @@ body[data-route^="Module"] .main-menu {
}
.sidebar-left .list-sidebar {
.stat-label,
.stat-no-records {
.sidebar-padding;
.list-sidebar {
.list-sidebar-label {
color: @text-muted;
text-transform: uppercase;
margin-bottom: 0;
font-size: @text-small;
}
.stat-label {
margin-bottom: -10px;
.group-by-count {
position:relative
}
.group-by-dropdown, .list-stats-dropdown {
max-height: 300px;
overflow-y: auto;
max-width: 200px;
}
.dropdown-search {
padding: 8px;
}
}

View file

@ -214,6 +214,8 @@ def revert(name, reason):
if doc_to_revert.type != 'Auto':
frappe.throw(_('This document cannot be reverted'))
if doc_to_revert.reverted: return
doc_to_revert.reverted = 1
doc_to_revert.save(ignore_permissions=True)

View file

@ -118,6 +118,34 @@ class TestEnergyPointLog(unittest.TestCase):
# no points for admin
self.assertEquals(points_after_closing_todo, 0)
def test_revert_points_on_cancelled_doc(self):
frappe.set_user('test@example.com')
create_energy_point_rule_for_todo()
created_todo = create_a_todo()
created_todo.status = 'Closed'
created_todo.save()
energy_point_logs = frappe.get_all('Energy Point Log')
self.assertEquals(len(energy_point_logs), 1)
# for submit and cancel permission
frappe.set_user('Administrator')
# submit
created_todo.docstatus = 1
created_todo.save()
# cancel
created_todo.docstatus = 2
created_todo.save()
energy_point_logs = frappe.get_all('Energy Point Log', fields=['reference_name', 'type', 'reverted'])
self.assertListEqual(energy_point_logs, [
{'reference_name': created_todo.name, 'type': 'Revert', 'reverted': 0},
{'reference_name': created_todo.name, 'type': 'Auto', 'reverted': 1}
])
def create_energy_point_rule_for_todo(multiplier_field=None):
name = 'ToDo Closed'
point_rule = frappe.db.get_all(

View file

@ -4,10 +4,11 @@
from __future__ import unicode_literals
import frappe
from frappe import _
import frappe.cache_manager
from frappe.model.document import Document
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
from frappe.social.doctype.energy_point_log.energy_point_log import create_energy_points_log
from frappe.social.doctype.energy_point_log.energy_point_log import create_energy_points_log, revert
class EnergyPointRule(Document):
def on_update(self):
@ -51,10 +52,27 @@ def process_energy_points(doc, state):
or not is_energy_point_enabled()):
return
old_doc = doc.get_doc_before_save()
# check if doc has been cancelled
if old_doc and old_doc.docstatus == 1 and doc.docstatus == 2:
return revert_points_for_cancelled_doc(doc)
for d in frappe.cache_manager.get_doctype_map('Energy Point Rule', doc.doctype,
dict(reference_doctype = doc.doctype, enabled=1)):
frappe.get_doc('Energy Point Rule', d.get('name')).apply(doc)
def revert_points_for_cancelled_doc(doc):
energy_point_logs = frappe.get_all('Energy Point Log', {
'reference_doctype': doc.doctype,
'reference_name': doc.name,
'type': 'Auto'
})
for log in energy_point_logs:
revert(log.name, _('Reference document has been cancelled'))
def get_energy_point_doctypes():
return [
d.reference_doctype for d in frappe.get_all('Energy Point Rule',

View file

@ -0,0 +1,8 @@
<p>{{ auto_repeat_failed_for }}</p>
<p>{{ _("The Auto Repeat for this document has been disabled.") }}</p>
<p>{{ error_log_message }}</p>
<div class="more-info">
{{_("This email is autogenerated")}}
</div>

View file

@ -0,0 +1,8 @@
<p>{{_("User {0} has requested for data deletion").format(user)}}.</p>
<p>{{_("Click on the link below to approve the request")}}.</p>
<p style="margin: 30px 0px;">
<a href="{{ url }}" rel="nofollow" class="btn btn-primary btn-sm primary-action" style="padding: 8px 20px;">
{{ _("Confirm Request") }}
</a>
</p>

Some files were not shown because too many files have changed in this diff Show more