fix: Merge branch 'develop' of https://github.com/frappe/frappe into offline-erpnext

This commit is contained in:
Rucha Mahabal 2020-01-03 12:04:10 +05:30
commit 44b0049aff
44 changed files with 829 additions and 2835 deletions

View file

@ -0,0 +1,63 @@
context('Depends On', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk');
});
before(() => {
cy.login();
cy.visit('/desk');
cy.window().its('frappe').then(frappe => {
frappe.call('frappe.tests.ui_test_helpers.create_doctype', {
name: 'Test Depends On',
fields: [
{
"label": "Test Field",
"fieldname": "test_field",
"fieldtype": "Data",
},
{
"label": "Dependant Field",
"fieldname": "dependant_field",
"fieldtype": "Data",
"mandatory_depends_on": "eval:doc.test_field=='Some Value'",
"read_only_depends_on": "eval:doc.test_field=='Some Other Value'",
},
{
"label": "Display Dependant Field",
"fieldname": "display_dependant_field",
"fieldtype": "Data",
'depends_on': "eval:doc.test_field=='Value'"
},
]
});
});
});
it('should set the field as mandatory depending on other fields value', () => {
cy.new_form('Test Depends On');
cy.fill_field('test_field', 'Some Value');
cy.get('button.primary-action').contains('Save').click();
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('be.visible');
cy.get('body').click();
cy.fill_field('test_field', 'Random value');
cy.get('button.primary-action').contains('Save').click();
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('not.be.visible');
});
it('should set the field as read only depending on other fields value', () => {
cy.new_form('Test Depends On');
cy.fill_field('dependant_field', 'Some Value');
cy.fill_field('test_field', 'Some Other Value');
cy.get('body').click();
cy.get('.control-input [data-fieldname="dependant_field"]').should('be.disabled');
cy.fill_field('test_field', 'Random Value');
cy.get('body').click();
cy.get('.control-input [data-fieldname="dependant_field"]').should('not.be.disabled');
});
it('should display the field depending on other fields value', () => {
cy.new_form('Test Depends On');
cy.get('.control-input [data-fieldname="display_dependant_field"]').should('not.be.visible');
cy.get('.control-input [data-fieldname="test_field"]').clear();
cy.fill_field('test_field', 'Value');
cy.get('body').click();
cy.get('.control-input [data-fieldname="display_dependant_field"]').should('be.visible');
});
});

View file

@ -9,7 +9,7 @@ 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.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day, month_diff
from frappe.model.document import Document
from frappe.core.doctype.communication.email import make
from frappe.utils.background_jobs import get_jobs
@ -48,7 +48,7 @@ class AutoRepeat(Document):
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)
self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, self.end_date)
def unlink_if_applicable(self):
if self.status == 'Completed' or self.disabled:
@ -107,27 +107,27 @@ class AutoRepeat(Document):
end_date = getdate(self.end_date)
if not self.end_date:
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
next_date = get_next_schedule_date(start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day)
row = {
"reference_document": self.reference_document,
"frequency": self.frequency,
"next_scheduled_date": start_date
"next_scheduled_date": next_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, for_full_schedule=True)
while (getdate(start_date) < getdate(end_date)):
next_date = get_next_schedule_date(
start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, for_full_schedule=True)
while (getdate(next_date) < getdate(end_date)):
row = {
"reference_document" : self.reference_document,
"frequency" : self.frequency,
"next_scheduled_date" : start_date
"next_scheduled_date" : next_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, for_full_schedule=True)
next_date = get_next_schedule_date(
next_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True)
return schedule_details
@ -268,8 +268,12 @@ class AutoRepeat(Document):
)
def get_next_schedule_date(start_date, frequency, repeat_on_day, repeat_on_last_day=False, end_date=None, for_full_schedule=False):
month_count = month_map.get(frequency)
def get_next_schedule_date(schedule_date, frequency, start_date, repeat_on_day=None, repeat_on_last_day=False, end_date=None, for_full_schedule=False):
if month_map.get(frequency):
month_count = month_map.get(frequency) + month_diff(schedule_date, start_date) - 1
else:
month_count = 0
day_count = 0
if month_count and repeat_on_last_day:
next_date = get_next_date(start_date, month_count, 31)
@ -288,7 +292,9 @@ def get_next_schedule_date(start_date, frequency, repeat_on_day, repeat_on_last_
# next schedule date should be after or on current date
if not for_full_schedule:
while getdate(next_date) < getdate(today()):
next_date = get_next_date(next_date, month_count, day_count)
if month_count:
month_count += month_map.get(frequency)
next_date = get_next_date(start_date, month_count, day_count)
return next_date
@ -316,8 +322,7 @@ def create_repeated_entries(data):
if 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)
schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.start_date, 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)

View file

@ -2,7 +2,7 @@
# MIT License. See license.txt
from __future__ import unicode_literals, print_function
import os, frappe, json, shutil, re, warnings
import os, frappe, json, shutil, re, warnings, tempfile
from os.path import exists as path_exists, join as join_path, abspath, isdir
from distutils.spawn import find_executable
from six import iteritems, text_type
@ -12,6 +12,51 @@ from frappe.utils.minify import JavascriptMinify
Build the `public` folders and setup languages
"""
def symlink(target, link_name, overwrite=False):
'''
Create a symbolic link named link_name pointing to target.
If link_name exists then FileExistsError is raised, unless overwrite=True.
When trying to overwrite a directory, IsADirectoryError is raised.
Source: https://stackoverflow.com/a/55742015/10309266
'''
if not overwrite:
os.symlink(target, linkname)
return
# os.replace() may fail if files are on different filesystems
link_dir = os.path.dirname(link_name)
# Create link to target with temporary filename
while True:
temp_link_name = tempfile.mktemp(dir=link_dir)
# os.* functions mimic as closely as possible system functions
# The POSIX symlink() returns EEXIST if link_name already exists
# https://pubs.opengroup.org/onlinepubs/9699919799/functions/symlink.html
try:
os.symlink(target, temp_link_name)
break
except FileExistsError:
pass
# Replace link_name with temp_link_name
try:
# Pre-empt os.replace on a directory with a nicer message
if os.path.isdir(link_name):
raise IsADirectoryError("Cannot symlink over existing directory: '{}'".format(link_name))
try:
os.replace(temp_link_name, link_name)
except AttributeError:
os.renames(temp_link_name, link_name)
except:
if os.path.islink(temp_link_name):
os.remove(temp_link_name)
raise
app_paths = None
def setup():
global app_paths
@ -118,7 +163,7 @@ def make_asset_dirs(make_copy=False, restore=False):
else:
shutil.rmtree(target)
try:
os.symlink(source, target)
symlink(source, target, overwrite=True)
except OSError:
print('Cannot link {} to {}'.format(source, target))
else:

View file

@ -1,17 +1,14 @@
from __future__ import unicode_literals
# imports - standard imports
import json
# imports - module imports
from frappe.model.document import Document
from frappe import _
from frappe.model.document import Document
from frappe import _
import frappe
# imports - frappe module imports
from frappe.chat import authenticate
from frappe.chat import authenticate
from frappe.core.doctype.version.version import get_diff
from frappe.chat.doctype.chat_message import chat_message
from frappe.chat.doctype.chat_message import chat_message
from frappe.chat.util import (
safe_json_loads,
dictify,
@ -22,13 +19,14 @@ from frappe.chat.util import (
session = frappe.session
def is_direct(owner, other, bidirectional = False):
def is_direct(owner, other, bidirectional=False):
def get_room(owner, other):
room = frappe.get_all('Chat Room', filters = [
['Chat Room', 'type' , 'in', ('Direct', 'Visitor')],
['Chat Room', 'owner', '=' , owner],
['Chat Room User', 'user' , '=' , other]
], distinct = True)
room = frappe.get_all('Chat Room', filters=[
['Chat Room', 'type', 'in', ('Direct', 'Visitor')],
['Chat Room', 'owner', '=', owner],
['Chat Room User', 'user', '=', other]
], distinct=True)
return room
@ -38,7 +36,8 @@ def is_direct(owner, other, bidirectional = False):
return exists
def get_chat_room_user_set(users, filter_ = None):
def get_chat_room_user_set(users, filter_=None):
seen, uset = set(), list()
for u in users:
@ -48,12 +47,13 @@ def get_chat_room_user_set(users, filter_ = None):
return uset
class ChatRoom(Document):
def validate(self):
if self.is_new():
users = get_chat_room_user_set(self.users, filter_ = lambda u: u.user != session.user)
users = get_chat_room_user_set(self.users, filter_=lambda u: u.user != session.user)
self.update(dict(
users = users
users=users
))
if self.type == "Direct":
@ -63,7 +63,7 @@ class ChatRoom(Document):
other = squashify(self.users)
if self.is_new():
if is_direct(self.owner, other.user, bidirectional = True):
if is_direct(self.owner, other.user, bidirectional=True):
frappe.throw(_('Direct room with {0} already exists.').format(other.user))
if self.type == "Group" and not self.room_name:
@ -74,40 +74,44 @@ class ChatRoom(Document):
before = self.get_doc_before_save()
if not before: return
after = self
diff = dictify(get_diff(before, after))
after = self
diff = dictify(get_diff(before, after))
if diff:
update = { }
update = {}
for changed in diff.changed:
field, old, new = changed
if field == 'last_message':
new = chat_message.get(new)
update.update({ field: new })
update.update({field: new})
if diff.added or diff.removed:
update.update(dict(users = [u.user for u in self.users]))
update.update(dict(users=[u.user for u in self.users]))
update = dict(room = self.name, data = update)
update = dict(room=self.name, data=update)
frappe.publish_realtime('frappe.chat.room:update', update, room = self.name, after_commit = True)
frappe.publish_realtime('frappe.chat.room:update', update, room=self.name,
after_commit=True)
@frappe.whitelist(allow_guest = True)
def get(user, rooms = None, fields = None, filters = None):
@frappe.whitelist(allow_guest=True)
def get(user=None, token=None, rooms=None, fields=None, filters=None):
# There is this horrible bug out here.
# Looks like if frappe.call sends optional arguments (not in right order), the argument turns to an empty string.
# Looks like if frappe.call sends optional arguments (not in right order),
# the argument turns to an empty string.
# I'm not even going to think searching for it.
# Hence, the hack was get_if_empty (previous assign_if_none)
# - Achilles Rasquinha achilles@frappe.io
authenticate(user)
data = user or token
authenticate(data)
rooms, fields, filters = safe_json_loads(rooms, fields, filters)
rooms = listify(get_if_empty(rooms, [ ]))
fields = listify(get_if_empty(fields, [ ]))
rooms = listify(get_if_empty(rooms, []))
fields = listify(get_if_empty(fields, []))
const = [ ] # constraints
const = [] # constraints
if rooms:
const.append(['Chat Room', 'name', 'in', rooms])
if filters:
@ -117,24 +121,24 @@ def get(user, rooms = None, fields = None, filters = None):
const.append(filters)
default = ['name', 'type', 'room_name', 'creation', 'owner', 'avatar']
handle = ['users', 'last_message']
handle = ['users', 'last_message']
param = [f for f in fields if f not in handle]
param = [f for f in fields if f not in handle]
rooms = frappe.get_all('Chat Room',
or_filters = [
['Chat Room', 'owner', '=', user],
['Chat Room User', 'user', '=', user]
],
filters = const,
fields = param + ['name'] if param else default,
distinct = True
)
rooms = frappe.get_all('Chat Room',
or_filters=[
['Chat Room', 'owner', '=', frappe.session.user],
['Chat Room User', 'user', '=', frappe.session.user]
],
filters=const,
fields=param + ['name'] if param else default,
distinct=True
)
if not fields or 'users' in fields:
for i, r in enumerate(rooms):
droom = frappe.get_doc('Chat Room', r.name)
rooms[i]['users'] = [ ]
rooms[i]['users'] = []
for duser in droom.users:
rooms[i]['users'].append(duser.user)
@ -151,46 +155,47 @@ def get(user, rooms = None, fields = None, filters = None):
return rooms
@frappe.whitelist(allow_guest = True)
def create(kind, owner, users = None, name = None):
authenticate(owner)
users = safe_json_loads(users)
@frappe.whitelist(allow_guest=True)
def create(kind, token, users=None, name=None):
authenticate(token)
users = safe_json_loads(users)
create = True
if kind == 'Visitor':
room = squashify(frappe.db.sql("""
SELECT name
FROM `tabChat Room`
WHERE owner = "{owner}"
""".format(owner = owner), as_dict = True))
WHERE owner=%s
""", (frappe.session.user), as_dict=True))
if room:
room = frappe.get_doc('Chat Room', room.name)
room = frappe.get_doc('Chat Room', room.name)
create = False
if create:
room = frappe.new_doc('Chat Room')
room.type = kind
room.owner = owner
room = frappe.new_doc('Chat Room')
room.type = kind
room.owner = frappe.session.user
room.room_name = name
dusers = [ ]
dusers = []
if kind != 'Visitor':
if users:
users = listify(users)
users = listify(users)
for user in users:
duser = frappe.new_doc('Chat Room User')
duser = frappe.new_doc('Chat Room User')
duser.user = user
dusers.append(duser)
room.users = dusers
else:
dsettings = frappe.get_single('Website Settings')
dsettings = frappe.get_single('Website Settings')
room.room_name = dsettings.chat_room_name
users = [user for user in room.users] if hasattr(room, 'users') else [ ]
users = [user for user in room.users] if hasattr(room, 'users') else []
for user in dsettings.chat_operators:
if user.user not in users:
@ -199,24 +204,26 @@ def create(kind, owner, users = None, name = None):
chat_room_user = {"doctype": "Chat Room User", "user": user.user}
room.append('users', chat_room_user)
room.save(ignore_permissions = True)
room.save(ignore_permissions=True)
room = get(owner, rooms = room.name)
users = [room.owner] + [u for u in room.users]
room = get(token=token, rooms=room.name)
if room:
users = [room.owner] + [u for u in room.users]
for u in users:
frappe.publish_realtime('frappe.chat.room:create', room, user = u, after_commit = True)
for user in users:
frappe.publish_realtime('frappe.chat.room:create', room, user=user, after_commit=True)
return room
@frappe.whitelist(allow_guest = True)
def history(room, user, fields = None, limit = 10, start = None, end = None):
@frappe.whitelist(allow_guest=True)
def history(room, user, fields=None, limit=10, start=None, end=None):
if frappe.get_doc('Chat Room', room).type != 'Visitor':
authenticate(user)
fields = safe_json_loads(fields)
mess = chat_message.history(room, limit = limit, start = start, end = end)
mess = squashify(mess)
mess = chat_message.history(room, limit=limit, start=start, end=end)
mess = squashify(mess)
return dictify(mess)
return dictify(mess)

View file

@ -351,16 +351,26 @@ def get_contacts(email_strings):
email = get_email_without_link(email)
contact_name = get_contact_name(email)
if not contact_name:
contact = frappe.get_doc({
"doctype": "Contact",
"first_name": frappe.unscrub(email.split("@")[0]),
})
contact.add_email(email_id=email, is_primary=True)
contact.insert(ignore_permissions=True)
contact_name = contact.name
if not contact_name and email:
email_parts = email.split("@")
first_name = frappe.unscrub(email_parts[0])
contacts.append(contact_name)
try:
contact_name = '{0}-{1}'.format(first_name, email_parts[1]) if first_name == 'Contact' else first_name
contact = frappe.get_doc({
"doctype": "Contact",
"first_name": contact_name,
"name": contact_name
})
contact.add_email(email_id=email, is_primary=True)
contact.insert(ignore_permissions=True)
contact_name = contact.name
except Exception:
traceback = frappe.get_traceback()
frappe.log_error(traceback)
if contact_name:
contacts.append(contact_name)
return contacts

View file

@ -238,8 +238,9 @@ def get_recipients_cc_and_bcc(doc, recipients, cc, bcc, fetched_from_email_accou
return recipients, cc, bcc
def remove_administrator_from_email_list(email_list):
if 'Administrator' in email_list:
email_list.remove('Administrator')
administrator_email = list(filter(lambda emails: "Administrator" in emails, email_list))
if administrator_email:
email_list.remove(administrator_email[0])
def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None):
"""Prepare to make multipart MIME Email
@ -304,27 +305,12 @@ def set_incoming_outgoing_accounts(doc):
doc.incoming_email_account = frappe.db.get_value("Email Account",
{"append_to": doc.reference_doctype, }, "email_id")
doc.outgoing_email_account = frappe.db.get_value("Email Account",
{"append_to": doc.reference_doctype, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender", "name",
"always_use_account_name_as_sender_name"], as_dict=True)
if not doc.incoming_email_account:
doc.incoming_email_account = frappe.db.get_value("Email Account",
{"default_incoming": 1, "enable_incoming": 1}, "email_id")
if not doc.outgoing_email_account:
# if from address is not the default email account
doc.outgoing_email_account = frappe.db.get_value("Email Account",
{"email_id": doc.sender, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender", "name",
"send_unsubscribe_message", "always_use_account_name_as_sender_name"], as_dict=True) or frappe._dict()
if not doc.outgoing_email_account:
doc.outgoing_email_account = frappe.db.get_value("Email Account",
{"default_outgoing": 1, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender", "name",
"send_unsubscribe_message", "always_use_account_name_as_sender_name"],as_dict=True) or frappe._dict()
doc.outgoing_email_account = frappe.email.smtp.get_outgoing_email_account(raise_exception_not_set=False,
append_to=doc.doctype, sender=doc.sender)
if doc.sent_or_received == "Sent":
doc.db_set("email_account", doc.outgoing_email_account.name)
@ -543,4 +529,4 @@ def mark_email_as_seen(name=None):
frappe.response["type"] = 'binary'
frappe.response["filename"] = "imaginary_pixel.png"
frappe.response["filecontent"] = buffered_obj.getvalue()
frappe.response["filecontent"] = buffered_obj.getvalue()

File diff suppressed because it is too large Load diff

View file

@ -905,7 +905,7 @@ def validate_fields(meta):
def check_illegal_depends_on_conditions(docfield):
''' assignment operation should not be allowed in the depends on condition.'''
depends_on_fields = ["depends_on", "collapsible_depends_on"]
depends_on_fields = ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"]
for field in depends_on_fields:
depends_on = docfield.get(field, None)
if depends_on and ("=" in depends_on) and \

View file

@ -96,14 +96,19 @@ class TestDocType(unittest.TestCase):
def test_all_depends_on_fields_conditions(self):
import re
docfields = frappe.get_all("DocField", or_filters={
docfields = frappe.get_all("DocField",
or_filters={
"ifnull(depends_on, '')": ("!=", ''),
"ifnull(collapsible_depends_on, '')": ("!=", '')
}, fields=["parent", "depends_on", "collapsible_depends_on", "fieldname", "fieldtype"])
"ifnull(collapsible_depends_on, '')": ("!=", ''),
"ifnull(mandatory_depends_on, '')": ("!=", ''),
"ifnull(read_only_depends_on, '')": ("!=", '')
},
fields=["parent", "depends_on", "collapsible_depends_on", "mandatory_depends_on",\
"read_only_depends_on", "fieldname", "fieldtype"])
pattern = """[\w\.:_]+\s*={1}\s*[\w\.@'"]+"""
for field in docfields:
for depends_on in ["depends_on", "collapsible_depends_on"]:
for depends_on in ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"]:
condition = field.get(depends_on)
if condition:
self.assertFalse(re.match(pattern, condition))

View file

@ -197,9 +197,9 @@ class File(Document):
def generate_content_hash(self):
if self.content_hash or not self.file_url or self.file_url.startswith('http'):
return
file_name = self.file_url.split('/')[-1]
try:
with open(get_files_path(self.file_name.lstrip("/"), is_private=self.is_private), "rb") as f:
with open(get_files_path(file_name, is_private=self.is_private), "rb") as f:
self.content_hash = get_content_hash(f.read())
except IOError:
frappe.msgprint(_("File {0} does not exist").format(self.file_url))
@ -311,39 +311,6 @@ class File(Document):
exists = os.path.exists(self.get_full_path())
return exists
def upload(self):
# get record details
self.attached_to_doctype = frappe.form_dict.doctype
self.attached_to_name = frappe.form_dict.docname
self.attached_to_field = frappe.form_dict.docfield
self.file_url = frappe.form_dict.file_url
self.file_name = frappe.form_dict.filename
frappe.form_dict.is_private = cint(frappe.form_dict.is_private)
if not self.file_name and not self.file_url:
frappe.msgprint(_("Please select a file or url"),
raise_exception=True)
file_doc = self.get_file_doc()
comment = {}
if self.attached_to_doctype and self.attached_to_name:
comment = frappe.get_doc(self.attached_to_doctype, self.attached_to_name).add_comment("Attachment",
_ ("added {0}").format("<a href='{file_url}' target='_blank'>{file_name}</a>{icon}".format(**{
"icon": ' <i class="fa fa-lock text-warning"></i>' \
if file_doc.is_private else "",
"file_url": file_doc.file_url.replace("#", "%23") \
if file_doc.file_name else file_doc.file_url,
"file_name": file_doc.file_name or file_doc.file_url
})))
return {
"name": file_doc.name,
"file_name": file_doc.file_name,
"file_url": file_doc.file_url,
"is_private": file_doc.is_private,
"comment": comment.as_dict() if comment else {}
}
def get_content(self):
"""Returns [`file_name`, `content`] for given file name `fname`"""

View file

@ -368,10 +368,10 @@ class User(Document):
(tab, field, '%s', field, '%s'), (new_name, old_name))
if frappe.db.exists("Chat Profile", old_name):
frappe.rename_doc("Chat Profile", old_name, new_name, force=True)
frappe.rename_doc("Chat Profile", old_name, new_name, force=True, show_alert=False)
if frappe.db.exists("Notification Settings", old_name):
frappe.rename_doc("Notification Settings", old_name, new_name, force=True)
frappe.rename_doc("Notification Settings", old_name, new_name, force=True, show_alert=False)
# set email
frappe.db.sql("""UPDATE `tabUser`

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"creation": "2013-01-10 16:34:01",
"description": "Adds a custom field to a DocType",
@ -24,10 +25,8 @@
"collapsible_depends_on",
"default",
"depends_on",
"description",
"permlevel",
"width",
"columns",
"mandatory_depends_on",
"read_only_depends_on",
"properties",
"reqd",
"unique",
@ -46,7 +45,11 @@
"report_hide",
"search_index",
"ignore_xss_filter",
"translatable"
"translatable",
"description",
"permlevel",
"width",
"columns"
],
"fields": [
{
@ -349,11 +352,24 @@
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
},
{
"fieldname": "mandatory_depends_on",
"fieldtype": "Code",
"label": "Mandatory Depends On",
"length": 255
},
{
"fieldname": "read_only_depends_on",
"fieldtype": "Code",
"label": "Read Only Depends On",
"length": 255
}
],
"icon": "fa fa-glass",
"idx": 1,
"modified": "2019-09-11 12:57:19.268934",
"links": [],
"modified": "2019-12-12 21:31:08.209996",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",

View file

@ -59,6 +59,8 @@ docfield_properties = {
'report_hide': 'Check',
'allow_on_submit': 'Check',
'translatable': 'Check',
'mandatory_depends_on': 'Data',
'read_only_depends_on': 'Data',
'depends_on': 'Data',
'description': 'Text',
'default': 'Text',
@ -68,7 +70,8 @@ docfield_properties = {
'columns': 'Int',
'remember_last_selected_value': 'Check',
'allow_bulk_edit': 'Check',
'auto_repeat': 'Link'
'auto_repeat': 'Link',
'allow_in_quick_entry': 'Check'
}
allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'),

View file

@ -40,6 +40,8 @@ CREATE TABLE `tabDocField` (
`show_preview_popup` int(1) NOT NULL DEFAULT 0,
`trigger` varchar(255) DEFAULT NULL,
`collapsible_depends_on` text,
`mandatory_depends_on` text,
`read_only_depends_on` text,
`depends_on` text,
`permlevel` int(11) NOT NULL DEFAULT 0,
`ignore_user_permissions` int(1) NOT NULL DEFAULT 0,

View file

@ -107,7 +107,7 @@ class PostgresDatabase(Database):
from information_schema.tables
where table_catalog='{0}'
and table_type = 'BASE TABLE'
and table_schema='public'""".format(frappe.conf.db_name))]
and table_schema='{1}'""".format(frappe.conf.db_name, frappe.conf.get("db_schema", "public")))]
def format_date(self, date):
if not date:

View file

@ -40,6 +40,8 @@ CREATE TABLE "tabDocField" (
"show_preview_popup" smallint NOT NULL DEFAULT 0,
"trigger" varchar(255) DEFAULT NULL,
"collapsible_depends_on" text,
"mandatory_depends_on" text,
"read_only_depends_on" text,
"depends_on" text,
"permlevel" bigint NOT NULL DEFAULT 0,
"ignore_user_permissions" smallint NOT NULL DEFAULT 0,

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:email_account_name",
"creation": "2014-09-11 12:04:34.163728",
@ -21,6 +22,7 @@
"use_imap",
"email_server",
"use_ssl",
"append_emails_to_sent_folder",
"incoming_port",
"attachment_limit",
"append_to",
@ -37,6 +39,7 @@
"enable_outgoing",
"smtp_server",
"use_tls",
"use_ssl_for_outgoing",
"smtp_port",
"default_outgoing",
"always_use_account_email_id_as_sender",
@ -389,10 +392,25 @@
"fieldname": "incoming_port",
"fieldtype": "Data",
"label": "Port"
},
{
"default": "0",
"depends_on": "eval:!doc.domain && doc.enable_outgoing",
"fieldname": "append_emails_to_sent_folder",
"fieldtype": "Check",
"label": "Append Emails to Sent Folder"
},
{
"default": "0",
"depends_on": "eval:!doc.domain && doc.enable_outgoing",
"fieldname": "use_ssl_for_outgoing",
"fieldtype": "Check",
"label": "Use SSL for Outgoing"
}
],
"icon": "fa fa-inbox",
"modified": "2019-08-31 18:01:15.568831",
"links": [],
"modified": "2019-12-18 15:56:39.744520",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Account",

View file

@ -7,6 +7,7 @@ import imaplib
import re
import json
import socket
import time
from frappe import _
from frappe.model.document import Document
from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html
@ -116,7 +117,8 @@ class EmailAccount(Document):
fields = [
"name as domain", "use_imap", "email_server",
"use_ssl", "smtp_server", "use_tls",
"smtp_port", "incoming_port"
"smtp_port", "incoming_port", "append_emails_to_sent_folder",
"use_ssl_for_outgoing"
]
return frappe.db.get_value("Email Domain", domain[1], fields, as_dict=True)
except Exception:
@ -128,11 +130,12 @@ class EmailAccount(Document):
if not self.smtp_server:
frappe.throw(_("{0} is required").format("SMTP Server"))
server = SMTPServer(login = getattr(self, "login_id", None) \
or self.email_id,
server = self.smtp_server,
port = cint(self.smtp_port),
use_tls = cint(self.use_tls)
server = SMTPServer(
login = getattr(self, "login_id", None) or self.email_id,
server=self.smtp_server,
port=cint(self.smtp_port),
use_tls=cint(self.use_tls),
use_ssl=cint(self.use_ssl_for_outgoing)
)
if self.password and not self.no_smtp_authentication:
server.password = self.get_password()
@ -648,6 +651,24 @@ class EmailAccount(Document):
if frappe.db.exists("Email Account", {"enable_automatic_linking": 1, "name": ('!=', self.name)}):
frappe.throw(_("Automatic Linking can be activated only for one Email Account."))
def append_email_to_sent_folder(self, message):
email_server = None
try:
email_server = self.get_incoming_server(in_receive=True)
except Exception:
frappe.log_error(title=_("Error while connecting to email account {0}").format(self.name))
if not email_server:
return
email_server.connect()
if email_server.imap:
email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message)
@frappe.whitelist()
def get_append_to(doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None):
if not txt: txt = ""

View file

@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "field:domain_name",
"creation": "2016-03-29 10:50:48.848239",
"doctype": "DocType",
@ -18,6 +19,8 @@
"outgoing_mail_settings",
"smtp_server",
"use_tls",
"use_ssl_for_outgoing",
"append_emails_to_sent_folder",
"smtp_port"
],
"fields": [
@ -30,7 +33,7 @@
"fieldtype": "Data",
"label": "domain name",
"read_only": 1,
"unique": 0
"unique": 1
},
{
"fieldname": "email_id",
@ -106,10 +109,23 @@
"fieldname": "incoming_port",
"fieldtype": "Data",
"label": "Port"
},
{
"default": "0",
"fieldname": "append_emails_to_sent_folder",
"fieldtype": "Check",
"label": "Append Emails to Sent Folder"
},
{
"default": "0",
"fieldname": "use_ssl_for_outgoing",
"fieldtype": "Check",
"label": "Use SSL for Outgoing"
}
],
"icon": "icon-inbox",
"modified": "2019-10-09 17:56:48.834704",
"links": [],
"modified": "2019-12-18 15:57:34.445308",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Domain",
@ -127,4 +143,4 @@
],
"sort_field": "modified",
"sort_order": "DESC"
}
}

View file

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import validate_email_address ,cint
from frappe.utils import validate_email_address ,cint, cstr
import imaplib,poplib,smtplib
from frappe.email.utils import get_port
@ -49,9 +49,16 @@ class EmailDomain(Document):
except Exception:
pass
try:
if self.use_tls and not self.smtp_port:
self.smtp_port = 587
sess = smtplib.SMTP((self.smtp_server or "").encode('utf-8'), cint(self.smtp_port) or None)
if self.use_ssl_for_outgoing:
if not self.smtp_port:
self.smtp_port = 465
sess = smtplib.SMTP_SSL((self.smtp_server or "").encode('utf-8'),
cint(self.smtp_port) or None)
else:
if self.use_tls and not self.smtp_port:
self.smtp_port = 587
sess = smtplib.SMTP(cstr(self.smtp_server or ""), cint(self.smtp_port) or None)
sess.quit()
except Exception:
frappe.throw(_("Outgoing email account not correct"))
@ -73,6 +80,8 @@ class EmailDomain(Document):
email_account.set("attachment_limit",self.attachment_limit)
email_account.set("smtp_server",self.smtp_server)
email_account.set("smtp_port",self.smtp_port)
email_account.set("use_ssl_for_outgoing", self.use_ssl_for_outgoing)
email_account.set("append_emails_to_sent_folder", self.append_emails_to_sent_folder)
email_account.save()
except Exception as e:
frappe.msgprint(email_account.name)

View file

@ -380,7 +380,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
for update''', email, as_dict=True)[0]
recipients_list = frappe.db.sql('''select name, recipient, status from
`tabEmail Queue Recipient` where parent=%s''',email.name,as_dict=1)
`tabEmail Queue Recipient` where parent=%s''', email.name, as_dict=1)
if frappe.are_emails_muted():
frappe.msgprint(_("Emails are muted"))
@ -401,8 +401,16 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
try:
message = None
if not frappe.flags.in_test:
if not smtpserver: smtpserver = SMTPServer()
if not smtpserver:
smtpserver = SMTPServer()
# to avoid always using default email account for outgoing
if getattr(frappe.local, "outgoing_email_account", None):
frappe.local.outgoing_email_account = {}
smtpserver.setup_email_account(email.reference_doctype, sender=email.sender)
for recipient in recipients_list:
@ -417,8 +425,10 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
frappe.db.sql("""update `tabEmail Queue Recipient` set status='Sent', modified=%s where name=%s""",
(now_datetime(), recipient.name), auto_commit=auto_commit)
email_sent_to_any_recipient = any("Sent" == s.status for s in recipients_list)
#if all are sent set status
if any("Sent" == s.status for s in recipients_list):
if email_sent_to_any_recipient:
frappe.db.sql("""update `tabEmail Queue` set status='Sent', modified=%s where name=%s""",
(now_datetime(), email.name), auto_commit=auto_commit)
else:
@ -430,6 +440,9 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
if email.communication:
frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
if smtpserver.append_emails_to_sent_folder and email_sent_to_any_recipient:
smtpserver.email_account.append_email_to_sent_folder(encode(message))
except (smtplib.SMTPServerDisconnected,
smtplib.SMTPConnectError,
smtplib.SMTPHeloError,
@ -439,7 +452,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
# bad connection/timeout, retry later
if any("Sent" == s.status for s in recipients_list):
if email_sent_to_any_recipient:
frappe.db.sql("""update `tabEmail Queue` set status='Partially Sent', modified=%s where name=%s""",
(now_datetime(), email.name), auto_commit=auto_commit)
else:
@ -459,7 +472,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
frappe.db.sql("""update `tabEmail Queue` set status='Not Sent', modified=%s, retry=retry+1 where name=%s""",
(now_datetime(), email.name), auto_commit=auto_commit)
else:
if any("Sent" == s.status for s in recipients_list):
if email_sent_to_any_recipient:
frappe.db.sql("""update `tabEmail Queue` set status='Partially Errored', error=%s where name=%s""",
(text_type(e), email.name), auto_commit=auto_commit)
else:

View file

@ -480,7 +480,7 @@ class Email:
"""Detect chartset."""
charset = part.get_content_charset()
if not charset:
charset = chardet.detect(cstr(part))['encoding']
charset = chardet.detect(safe_encode(cstr(part)))['encoding']
return charset

View file

@ -8,7 +8,7 @@ import smtplib
import email.utils
import _socket, sys
from frappe import _
from frappe.utils import cint, parse_addr
from frappe.utils import cint, cstr, parse_addr
def send(email, append_to=None, retry=1):
"""Deprecated: Send the message or add it to Outbox Email"""
@ -52,35 +52,38 @@ def get_outgoing_email_account(raise_exception_not_set=True, append_to=None, sen
or frappe.local.outgoing_email_account.get("default")):
email_account = None
if append_to:
# append_to is only valid when enable_incoming is checked
if sender_email_id:
# check if the sender has an email account with enable_outgoing
email_account = _get_email_account({"enable_outgoing": 1,
"email_id": sender_email_id})
# in case of multiple Email Accounts with same append_to
# narrow it down based on email_id
email_account = _get_email_account({
if not email_account and append_to:
# append_to is only valid when enable_incoming is checked
email_accounts = frappe.db.get_values("Email Account", {
"enable_outgoing": 1,
"enable_incoming": 1,
"append_to": append_to,
"email_id": sender_email_id
})
}, cache=True)
# else find the first Email Account with append_to
if not email_account:
if email_accounts:
_email_account = email_accounts[0]
else:
email_account = _get_email_account({
"enable_outgoing": 1,
"enable_incoming": 1,
"append_to": append_to
})
if not email_account and sender_email_id:
# check if the sender has email account with enable_outgoing
email_account = _get_email_account({"enable_outgoing": 1, "email_id": sender_email_id})
if not email_account:
# sender don't have the outging email account
sender_email_id = None
email_account = get_default_outgoing_email_account(raise_exception_not_set=raise_exception_not_set)
if not email_account and _email_account:
# if default email account is not configured then setup first email account based on append to
email_account = _email_account
if not email_account and raise_exception_not_set and cint(frappe.db.get_single_value('System Settings', 'setup_complete')):
frappe.throw(_("Please setup default Email Account from Setup > Email > Email Account"),
frappe.OutgoingEmailError)
@ -152,16 +155,19 @@ def _get_email_account(filters):
return frappe.get_doc("Email Account", name) if name else None
class SMTPServer:
def __init__(self, login=None, password=None, server=None, port=None, use_tls=None, append_to=None):
def __init__(self, login=None, password=None, server=None, port=None, use_tls=None, use_ssl=None, append_to=None):
# get defaults from mail settings
self._sess = None
self.email_account = None
self.server = None
self.append_emails_to_sent_folder = None
if server:
self.server = server
self.port = port
self.use_tls = cint(use_tls)
self.use_ssl = cint(use_ssl)
self.login = login
self.password = password
@ -183,6 +189,8 @@ class SMTPServer:
self.port = self.email_account.smtp_port
self.use_tls = self.email_account.use_tls
self.sender = self.email_account.email_id
self.use_ssl = self.email_account.use_ssl_for_outgoing
self.append_emails_to_sent_folder = self.email_account.append_emails_to_sent_folder
self.always_use_account_email_id_as_sender = cint(self.email_account.get("always_use_account_email_id_as_sender"))
self.always_use_account_name_as_sender_name = cint(self.email_account.get("always_use_account_name_as_sender_name"))
@ -199,11 +207,18 @@ class SMTPServer:
raise frappe.OutgoingEmailError(err_msg)
try:
if self.use_tls and not self.port:
self.port = 587
if self.use_ssl:
if not self.port:
self.smtp_port = 465
self._sess = smtplib.SMTP((self.server or "").encode('utf-8'),
cint(self.port) or None)
self._sess = smtplib.SMTP_SSL((self.server or "").encode('utf-8'),
cint(self.port) or None)
else:
if self.use_tls and not self.port:
self.port = 587
self._sess = smtplib.SMTP(cstr(self.server or ""),
cint(self.port) or None)
if not self._sess:
err_msg = _('Could not connect to outgoing email server')

View file

@ -106,13 +106,12 @@ def get_webhook_headers(doc, webhook):
def get_webhook_data(doc, webhook):
data = {}
doc = doc.as_dict(convert_dates_to_str=True)
if webhook.webhook_data:
for w in webhook.webhook_data:
value = doc.get(w.fieldname)
if isinstance(value, datetime.datetime):
value = frappe.utils.get_datetime_str(value)
data[w.key] = value
data = {w.key: doc.get(w.fieldname) for w in webhook.webhook_data}
elif webhook.webhook_json:
data = frappe.render_template(webhook.webhook_json, get_context(doc))
data = json.loads(data)
return data

View file

@ -275,7 +275,7 @@ class BaseDocument(object):
doc["doctype"] = self.doctype
for df in self.meta.get_table_fields():
children = self.get(df.fieldname) or []
doc[df.fieldname] = [d.as_dict(no_nulls=no_nulls) for d in children]
doc[df.fieldname] = [d.as_dict(convert_dates_to_str=convert_dates_to_str, no_nulls=no_nulls) for d in children]
if no_nulls:
for k in list(doc):

View file

@ -27,7 +27,7 @@ def update_document_title(doctype, docname, title_field=None, old_title=None, ne
@frappe.whitelist()
def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=False, ignore_if_exists=False):
def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=False, ignore_if_exists=False, show_alert=True):
"""
Renames a doc(dt, old) to doc(dt, new) and
updates all linked fields of type "Link"
@ -99,7 +99,9 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F
frappe.clear_cache()
frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', doctype=doctype)
frappe.msgprint(_('Document renamed from {0} to {1}').format(bold(old), bold(new)), alert=True, indicator='green')
if show_alert:
frappe.msgprint(_('Document renamed from {0} to {1}').format(bold(old), bold(new)), alert=True, indicator='green')
return new

View file

@ -105,6 +105,18 @@ def apply_workflow(doc, action):
return doc
@frappe.whitelist()
def can_cancel_document(doc):
doc = frappe.get_doc(frappe.parse_json(doc))
workflow = get_workflow(doc.doctype)
for state_doc in workflow.states:
if state_doc.doc_status == '2':
for transition in workflow.transitions:
if transition.next_state == state_doc.state:
return False
return True
return True
def validate_workflow(doc):
'''Validate Workflow State and Transition for the current user.

View file

@ -718,7 +718,7 @@ frappe.chat.room.create = function (kind, owner, users, name, fn) {
return new Promise(resolve => {
frappe.call("frappe.chat.doctype.chat_room.chat_room.create",
{ kind: kind, owner: owner || frappe.session.user, users: users, name: name },
{ kind: kind, token: owner || frappe.session.user, users: users, name: name },
r => {
let room = r.message
room = { ...room, creation: new frappe.datetime.datetime(room.creation) }

View file

@ -45,7 +45,8 @@ frappe.ui.form.ControlTime = frappe.ui.form.ControlDate.extend({
&& ((this.last_value && this.last_value !== this.value)
|| (!this.datepicker.selectedDates.length))) {
var date_obj = frappe.datetime.moment_to_date_obj(moment(value, frappe.sys_defaults['time_format']));
let time_format = frappe.sys_defaults.time_format || 'HH:mm:ss';
var date_obj = frappe.datetime.moment_to_date_obj(moment(value, time_format));
this.datepicker.selectDate(date_obj);
}
},

View file

@ -377,11 +377,11 @@ frappe.ui.form.Timeline = class Timeline {
c["edit"] = "";
if(c.communication_type=="Comment" && (c.comment_type || "Comment") === "Comment") {
if(frappe.model.can_delete("Comment")) {
c["delete"] = '<a class="close delete-comment" title="Delete" href="#"><i class="octicon octicon-x"></i></a>';
c["delete"] = `<a class="close delete-comment" title="${__('Delete')}" href="#"><i class="octicon octicon-x"></i></a>`;
}
if(frappe.user.name == c.sender || (frappe.user.name == 'Administrator')) {
c["edit"] = '<a class="edit-comment text-muted" title="Edit" href="#">Edit</a>';
c["edit"] = `<a class="edit-comment text-muted" title="${__('Edit')}" href="#">${__('Edit')}</a>`;
}
}
let communication_date = c.communication_date || c.creation;

View file

@ -84,7 +84,7 @@ frappe.form.formatters = {
},
Check: function(value) {
if(value) {
return '<i class="octicon octicon-check" style="margin-right: 3px;"></i>';
return '<i class="fa fa-check" style="margin-right: 3px;"></i>';
} else {
return '<i class="fa fa-square disabled-check"></i>';
}

View file

@ -451,27 +451,27 @@ frappe.ui.form.Layout = Class.extend({
// build dependants' dictionary
var has_dep = false;
for(var fkey in this.fields_list) {
for (var fkey in this.fields_list) {
var f = this.fields_list[fkey];
f.dependencies_clear = true;
if(f.df.depends_on) {
if (f.df.depends_on || f.df.mandatory_depends_on || f.df.read_only_depends_on) {
has_dep = true;
}
}
if(!has_dep)return;
if (!has_dep) return;
// show / hide based on values
for(var i=me.fields_list.length-1;i>=0;i--) {
for (var i=me.fields_list.length-1;i>=0;i--) {
var f = me.fields_list[i];
f.guardian_has_value = true;
if(f.df.depends_on) {
if (f.df.depends_on) {
// evaluate guardian
f.guardian_has_value = this.evaluate_depends_on_value(f.df.depends_on);
// show / hide
if(f.guardian_has_value) {
if (f.guardian_has_value) {
if(f.df.hidden_due_to_dependency) {
f.df.hidden_due_to_dependency = false;
f.refresh();
@ -483,10 +483,28 @@ frappe.ui.form.Layout = Class.extend({
}
}
}
if (f.df.mandatory_depends_on) {
this.set_dependant_property(f.df.mandatory_depends_on, f.df.fieldname, 'reqd');
}
if (f.df.read_only_depends_on) {
this.set_dependant_property(f.df.read_only_depends_on, f.df.fieldname, 'read_only');
}
}
this.refresh_section_count();
},
set_dependant_property: function(condition, fieldname, property) {
let set_property = this.evaluate_depends_on_value(condition);
if (this.frm) {
if (set_property) {
this.frm.set_df_property(fieldname, property, 1);
} else {
this.frm.set_df_property(fieldname, property, 0);
}
}
},
evaluate_depends_on_value: function(expression) {
var out = null;
var doc = this.doc;

View file

@ -105,7 +105,17 @@ frappe.ui.form.States = Class.extend({
});
}
});
this.setup_btn(added);
if (!added) {
//call function and clear cancel button if Cancel doc state is defined in the workfloe
frappe.xcall('frappe.model.workflow.can_cancel_document', {doc: this.frm.doc}).then((can_cancel) => {
if (!can_cancel) {
this.frm.page.clear_secondary_action();
}
});
} else {
this.setup_btn(added);
}
});
},

View file

@ -90,7 +90,7 @@ frappe.views.ListGroupBy = class ListGroupBy {
this.render_dropdown_items(field_count_list, dropdown);
this.sidebar.setup_dropdown_search(dropdown, '.group-by-value');
} else {
dropdown.find('.group-by-loading').hide();
dropdown.find('.group-by-loading').html(`${__("No filters found")}`);
}
});
});

View file

@ -116,6 +116,7 @@ export default {
user_section = [
{
fieldname: 'user_section',
fieldtype: 'Section Break',
depends_on: doc => doc.setup_for === user_value
}
@ -134,6 +135,7 @@ export default {
global_section = [
{
fieldname: 'global_section',
fieldtype: 'Section Break',
depends_on: doc => doc.setup_for === 'Everyone'
}
@ -188,8 +190,11 @@ export default {
update_global_modules(d) {
let blocked_modules = [];
for (let category of this.module_categories) {
let unchecked_options = d.get_field(`global:${category}`).get_unchecked_options();
blocked_modules = blocked_modules.concat(unchecked_options);
let field = d.get_field(`global:${category}`);
if (field) {
let unchecked_options = field.get_unchecked_options();
blocked_modules = blocked_modules.concat(unchecked_options);
}
}
frappe.call({

View file

@ -204,7 +204,7 @@
.navbar-form .awesomplete {
margin-left: -15px;
width: 300px;
width: 370px;
@media (max-width: @screen-md) {
width: 280px;

View file

@ -2,7 +2,7 @@
<h3>{{ _('Top Performer') }} 🏆 </h3>
<p> {{ frappe.get_fullname(top_performer.user) }}
<span class="text-muted">
{{ frappe._('gained {0} points').format(frappe.utils.cint(top_performer.energy_points)) }}
{{ _('gained {0} points').format(frappe.utils.cint(top_performer.energy_points)) }}
</span>
</p>
{% endif %}
@ -11,7 +11,7 @@
<h3>{{ _('Top Reviewer') }} ❤️ </h3>
<p> {{ frappe.get_fullname(top_reviewer.user) }}
<span class="text-muted">
{{ frappe._('gave {0} points').format(frappe.utils.cint(top_reviewer.given_points)) }}
{{ _('gave {0} points').format(frappe.utils.cint(top_reviewer.given_points)) }}
</span>
</p>
@ -24,9 +24,9 @@
<table class='table table-bordered'>
<tr>
<th> # </th>
<th style='width: 70%'>{{ frappe._('User') }}</th>
<th style='width: 15%'>{{ frappe._('Energy Points') }}</th>
<th style='width: 15%'>{{ frappe._('Points Given') }}</th>
<th style='width: 70%'>{{ _('User') }}</th>
<th style='width: 15%'>{{ _('Energy Points') }}</th>
<th style='width: 15%'>{{ _('Points Given') }}</th>
</tr>
{% for user in standings %}
<tr>

View file

@ -49,6 +49,10 @@
}
}
.disabled-check {
color: #eee;
}
.data-field {
margin-top: 5px;
margin-bottom: 5px;

View file

@ -75,6 +75,23 @@ def create_contact_phone_nos_records():
doc.append('phone_nos', {'phone': '123456{}'.format(index)})
doc.insert()
@frappe.whitelist()
def create_doctype(name, fields):
fields = frappe.parse_json(fields)
if frappe.db.exists('DocType', name):
return
frappe.get_doc({
"doctype": "DocType",
"module": "Core",
"custom": 1,
"fields": fields,
"permissions": [{
"role": "System Manager",
"read": 1
}],
"name": name
}).insert()
@frappe.whitelist()
def create_contact_records():
if frappe.db.get_all('Contact', {'first_name': 'Test Form Contact 1'}):

View file

@ -16,6 +16,7 @@ def get_jenv():
set_filters(jenv)
jenv.globals.update(get_safe_globals())
jenv.globals.update(get_jenv_customization('methods'))
frappe.local.jenv = jenv
@ -124,4 +125,27 @@ def set_filters(jenv):
jenv.filters["flt"] = flt
jenv.filters["abs_url"] = abs_url
if frappe.flags.in_setup_help: return
if frappe.flags.in_setup_help:
return
jenv.filters.update(get_jenv_customization('filters'))
def get_jenv_customization(customization_type):
'''Returns a dict with filter/method name as key and definition as value'''
import frappe
out = {}
if not getattr(frappe.local, "site", None):
return out
values = frappe.get_hooks("jenv", {}).get(customization_type)
if not values:
return out
for value in values:
fn_name, fn_string = value.split(":")
out[fn_name] = frappe.get_attr(fn_string)
return out

View file

@ -2,14 +2,24 @@
# MIT License. See license.txt
from __future__ import unicode_literals
import pdfkit, os, frappe
import io
import os
import re
from distutils.version import LooseVersion
from frappe.utils import scrub_urls, get_wkhtmltopdf_version
from frappe import _
import six, re, io
import pdfkit
import six
from bs4 import BeautifulSoup
from PyPDF2 import PdfFileReader, PdfFileWriter
import frappe
from frappe import _
from frappe.utils import get_wkhtmltopdf_version, scrub_urls
PDF_CONTENT_ERRORS = ["ContentNotFoundError", "ContentOperationNotPermittedError",
"UnknownContentError", "RemoteHostClosedError"]
def get_pdf(html, options=None, output=None):
html = scrub_urls(html)
html, options = prepare_options(html, options)
@ -30,20 +40,14 @@ def get_pdf(html, options=None, output=None):
# https://pythonhosted.org/PyPDF2/PdfFileReader.html
# create in-memory binary streams from filedata and create a PdfFileReader object
reader = PdfFileReader(io.BytesIO(filedata))
except IOError as e:
if ("ContentNotFoundError" in e.message
or "ContentOperationNotPermittedError" in e.message
or "UnknownContentError" in e.message
or "RemoteHostClosedError" in e.message):
except OSError as e:
if any([error in str(e) for error in PDF_CONTENT_ERRORS]):
if not filedata:
frappe.throw(_("PDF generation failed because of broken image links"))
# allow pdfs with missing images if file got created
if filedata:
if output: # output is a PdfFileWriter object
output.appendPagesFromReader(reader)
else:
frappe.throw(_("PDF generation failed because of broken image links"))
if output: # output is a PdfFileWriter object
output.appendPagesFromReader(reader)
else:
raise
@ -66,6 +70,7 @@ def get_pdf(html, options=None, output=None):
return filedata
def get_file_data_from_writer(writer_obj):
# https://docs.python.org/3/library/io.html
@ -112,6 +117,7 @@ def prepare_options(html, options):
return html, options
def read_options_from_html(html):
options = {}
soup = BeautifulSoup(html, "html5lib")
@ -132,6 +138,7 @@ def read_options_from_html(html):
return soup.prettify(), options
def prepare_header_footer(soup):
options = {}
@ -174,6 +181,7 @@ def prepare_header_footer(soup):
return options
def cleanup(fname, options):
if os.path.exists(fname):
os.remove(fname)
@ -182,6 +190,7 @@ def cleanup(fname, options):
if options.get(key) and os.path.exists(options[key]):
os.remove(options[key])
def toggle_visible_pdf(soup):
for tag in soup.find_all(attrs={"class": "visible-pdf"}):
# remove visible-pdf class to unhide

View file

@ -48,11 +48,9 @@ def get_safe_globals():
# make available limited methods of frappe
json=json,
dict=dict,
_dict=frappe._dict,
frappe=frappe._dict(
_=frappe._,
_dict=frappe._dict,
flags=frappe.flags,
format=frappe.format_value,
format_value=frappe.format_value,
date_format=date_format,

View file

@ -34,8 +34,7 @@ passlib==1.7.1
pdfkit==0.6.1
Pillow==6.2.1
premailer==3.6.1
psycopg2-binary==2.7.5
psycopg2==2.7.5
psycopg2-binary==2.8.4
pyasn1==0.4.7
Pygments==2.2.0
PyJWT==1.7.1
@ -64,4 +63,4 @@ urllib3==1.25.7
watchdog==0.8.0
Werkzeug==0.16.0
xlrd==1.2.0
zxcvbn-python==4.4.24
zxcvbn-python==4.4.24