Merge branch 'develop' into mandatory-depends-on
This commit is contained in:
commit
480a192324
49 changed files with 593 additions and 219 deletions
53
cypress/fixtures/custom_submittable_doctype.js
Normal file
53
cypress/fixtures/custom_submittable_doctype.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
export default {
|
||||
name: 'Custom Submittable DocType',
|
||||
custom: 1,
|
||||
actions: [],
|
||||
is_submittable: 1,
|
||||
creation: '2019-12-10 06:29:07.215072',
|
||||
doctype: 'DocType',
|
||||
editable_grid: 1,
|
||||
engine: 'InnoDB',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'enabled',
|
||||
fieldtype: 'Check',
|
||||
label: 'Enabled',
|
||||
allow_on_submit: 1,
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'title',
|
||||
fieldtype: 'Data',
|
||||
label: 'title',
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'description',
|
||||
fieldtype: 'Text Editor',
|
||||
label: 'Description'
|
||||
}
|
||||
],
|
||||
links: [],
|
||||
modified: '2019-12-10 14:40:53.127615',
|
||||
modified_by: 'Administrator',
|
||||
module: 'Custom',
|
||||
owner: 'Administrator',
|
||||
permissions: [
|
||||
{
|
||||
create: 1,
|
||||
delete: 1,
|
||||
email: 1,
|
||||
print: 1,
|
||||
read: 1,
|
||||
role: 'System Manager',
|
||||
share: 1,
|
||||
write: 1,
|
||||
submit: 1,
|
||||
cancel: 1
|
||||
}
|
||||
],
|
||||
quick_entry: 1,
|
||||
sort_field: 'modified',
|
||||
sort_order: 'ASC',
|
||||
track_changes: 1
|
||||
};
|
||||
|
|
@ -1,8 +1,4 @@
|
|||
context('Form', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk');
|
||||
});
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk');
|
||||
|
|
@ -10,6 +6,9 @@ context('Form', () => {
|
|||
frappe.call("frappe.tests.ui_test_helpers.create_contact_records");
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
cy.visit('/desk');
|
||||
});
|
||||
it('create a new form', () => {
|
||||
cy.visit('/desk#Form/ToDo/New ToDo 1');
|
||||
cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
|
||||
|
|
@ -27,10 +26,11 @@ context('Form', () => {
|
|||
cy.get('.filter-field .input-with-feedback.form-control').type('123', { force: true });
|
||||
cy.get('.filter-box .btn:contains("Apply")').click({ force: true });
|
||||
cy.visit('/desk#Form/Contact/Test Form Contact 3');
|
||||
cy.get('.prev-doc').click();
|
||||
cy.get('.prev-doc').click({ force: true });
|
||||
cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible');
|
||||
cy.get('.modal-backdrop').click();
|
||||
cy.get('.next-doc').click();
|
||||
cy.get('.btn-modal-close:visible').click();
|
||||
cy.get('.next-doc').click({ force: true });
|
||||
cy.wait(200);
|
||||
cy.contains('Test Form Contact 2').should('not.exist');
|
||||
cy.get('.page-title .title-text').should('contain', 'Test Form Contact 1');
|
||||
cy.visit('/desk#List/Contact');
|
||||
|
|
|
|||
|
|
@ -14,15 +14,15 @@ context('Grid Pagination', () => {
|
|||
cy.visit('/desk#Form/Contact/Test Contact');
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
cy.get('@table').find('.current-page-number').should('contain', '1');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '50');
|
||||
cy.get('@table').find('.grid-body .grid-row').should('have.length', 20);
|
||||
cy.get('@table').find('.total-page-number').should('contain', '20');
|
||||
cy.get('@table').find('.grid-body .grid-row').should('have.length', 50);
|
||||
});
|
||||
it('goes to the next and previous page', () => {
|
||||
cy.visit('/desk#Form/Contact/Test Contact');
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
cy.get('@table').find('.next-page').click();
|
||||
cy.get('@table').find('.current-page-number').should('contain', '2');
|
||||
cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '21');
|
||||
cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '51');
|
||||
cy.get('@table').find('.prev-page').click();
|
||||
cy.get('@table').find('.current-page-number').should('contain', '1');
|
||||
cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '1');
|
||||
|
|
@ -32,19 +32,20 @@ context('Grid Pagination', () => {
|
|||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('@table').find('.grid-body .row-index').should('contain', 1001);
|
||||
cy.get('@table').find('.current-page-number').should('contain', '51');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '51');
|
||||
cy.get('@table').find('.current-page-number').should('contain', '21');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '21');
|
||||
cy.get('@table').find('.grid-body .grid-row .grid-row-check').click({force: true});
|
||||
cy.get('@table').find('button.grid-remove-rows').click();
|
||||
cy.get('@table').find('.grid-body .row-index').last().should('contain', 1000);
|
||||
cy.get('@table').find('.current-page-number').should('contain', '50');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '50');
|
||||
cy.get('@table').find('.current-page-number').should('contain', '20');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '20');
|
||||
});
|
||||
it('deletes all rows', ()=> {
|
||||
cy.visit('/desk#Form/Contact/Test Contact');
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
cy.get('@table').find('.grid-heading-row .grid-row-check').click({force: true});
|
||||
cy.get('@table').find('button.grid-remove-all-rows').click();
|
||||
cy.get('.modal-dialog .btn-primary').contains('Yes').click();
|
||||
cy.get('@table').find('.grid-body .grid-row').should('have.length', 0);
|
||||
});
|
||||
});
|
||||
40
cypress/integration/report_view.js
Normal file
40
cypress/integration/report_view.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import custom_submittable_doctype from '../fixtures/custom_submittable_doctype';
|
||||
const doctype_name = custom_submittable_doctype.name;
|
||||
|
||||
context('Report View', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk');
|
||||
cy.insert_doc('DocType', custom_submittable_doctype, true);
|
||||
cy.clear_cache();
|
||||
cy.insert_doc(doctype_name, {
|
||||
'title': 'Doc 1',
|
||||
'description': 'Random Text',
|
||||
'enabled': 0,
|
||||
// submit document
|
||||
'docstatus': 1
|
||||
}, true).as('doc');
|
||||
});
|
||||
it('Field with enabled allow_on_submit should be editable.', () => {
|
||||
cy.server();
|
||||
cy.route('POST', 'api/method/frappe.client.set_value').as('value-update');
|
||||
cy.visit(`/desk#List/${doctype_name}/Report`);
|
||||
let cell = cy.get('.dt-row-0 > .dt-cell--col-3');
|
||||
// select the cell
|
||||
cell.dblclick();
|
||||
cell.find('input[data-fieldname="enabled"]').check({force: true});
|
||||
cy.get('.dt-row-0 > .dt-cell--col-4').click();
|
||||
cy.wait('@value-update');
|
||||
cy.get('@doc').then(doc => {
|
||||
cy.call('frappe.client.get_value', {
|
||||
doctype: doc.doctype,
|
||||
filters: {
|
||||
name: doc.name,
|
||||
},
|
||||
fieldname: 'enabled'
|
||||
}).then(r => {
|
||||
expect(r.message.enabled).to.equals(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -183,3 +183,31 @@ Cypress.Commands.add('hide_dialog', () => {
|
|||
cy.get_open_dialog().find('.btn-modal-close').click();
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => {
|
||||
return cy
|
||||
.window()
|
||||
.its('frappe.csrf_token')
|
||||
.then(csrf_token => {
|
||||
return cy
|
||||
.request({
|
||||
method: 'POST',
|
||||
url: `/api/resource/${doctype}`,
|
||||
body: args,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Frappe-CSRF-Token': csrf_token
|
||||
},
|
||||
failOnStatusCode: !ignore_duplicate
|
||||
})
|
||||
.then(res => {
|
||||
let status_codes = [200];
|
||||
if (ignore_duplicate) {
|
||||
status_codes.push(409);
|
||||
}
|
||||
expect(res.status).to.be.oneOf(status_codes);
|
||||
return res.body.data;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -23,7 +23,7 @@ if sys.version[0] == '2':
|
|||
reload(sys)
|
||||
sys.setdefaultencoding("utf-8")
|
||||
|
||||
__version__ = '12.0.20'
|
||||
__version__ = '12.1.0'
|
||||
__title__ = "Frappe Framework"
|
||||
|
||||
local = Local()
|
||||
|
|
|
|||
|
|
@ -31,12 +31,6 @@ class AssignmentRule(Document):
|
|||
|
||||
return False
|
||||
|
||||
def apply_close(self, doc, assignments):
|
||||
if (self.close_assignments and
|
||||
self.name in [d.assignment_rule for d in assignments]):
|
||||
return self.close_assignments(doc)
|
||||
|
||||
return False
|
||||
|
||||
def apply_assign(self, doc):
|
||||
if self.safe_eval('assign_condition', doc):
|
||||
|
|
@ -157,16 +151,17 @@ def bulk_apply(doctype, docnames):
|
|||
apply(None, doctype=doctype, name=name)
|
||||
|
||||
def reopen_closed_assignment(doc):
|
||||
todo = frappe.db.exists('ToDo', dict(
|
||||
todo_list = frappe.db.get_all('ToDo', filters = dict(
|
||||
reference_type = doc.doctype,
|
||||
reference_name = doc.name,
|
||||
status = 'Closed'
|
||||
))
|
||||
if not todo:
|
||||
if not todo_list:
|
||||
return False
|
||||
todo = frappe.get_doc("ToDo", todo)
|
||||
todo.status = 'Open'
|
||||
todo.save(ignore_permissions=True)
|
||||
for todo in todo_list:
|
||||
todo_doc = frappe.get_doc('ToDo', todo.name)
|
||||
todo_doc.status = 'Open'
|
||||
todo_doc.save(ignore_permissions=True)
|
||||
return True
|
||||
|
||||
def apply(doc, method=None, doctype=None, name=None):
|
||||
|
|
@ -225,13 +220,12 @@ def apply(doc, method=None, doctype=None, name=None):
|
|||
continue
|
||||
|
||||
if not new_apply:
|
||||
reopen = reopen_closed_assignment(doc)
|
||||
if reopen:
|
||||
break
|
||||
close = assignment_rule.apply_close(doc, assignments)
|
||||
if close:
|
||||
break
|
||||
|
||||
# only reopen if close condition is not satisfied
|
||||
if not assignment_rule.safe_eval('close_condition', doc):
|
||||
reopen = reopen_closed_assignment(doc)
|
||||
if reopen:
|
||||
break
|
||||
assignment_rule.close_assignments(doc)
|
||||
|
||||
def get_assignment_rules():
|
||||
return [d.document_type for d in frappe.db.get_all('Assignment Rule', fields=['document_type'], filters=dict(disabled = 0))]
|
||||
|
|
|
|||
|
|
@ -117,8 +117,8 @@ class AutoRepeat(Document):
|
|||
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 = 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)
|
||||
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)):
|
||||
row = {
|
||||
"reference_document" : self.reference_document,
|
||||
|
|
@ -126,10 +126,9 @@ class AutoRepeat(Document):
|
|||
"next_scheduled_date" : start_date
|
||||
}
|
||||
schedule_details.append(row)
|
||||
start_date = start_date = get_next_schedule_date(
|
||||
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)
|
||||
|
||||
|
||||
return schedule_details
|
||||
|
||||
def create_documents(self):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "Prompt",
|
||||
"creation": "2019-09-30 11:56:57.943241",
|
||||
"doctype": "DocType",
|
||||
|
|
@ -43,7 +44,7 @@
|
|||
"fieldname": "doctype_event",
|
||||
"fieldtype": "Select",
|
||||
"label": "DocType Event",
|
||||
"options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete"
|
||||
"options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.script_type==='API'",
|
||||
|
|
@ -73,7 +74,8 @@
|
|||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"modified": "2019-10-09 15:08:40.085059",
|
||||
"links": [],
|
||||
"modified": "2019-12-17 12:55:07.389775",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Server Script",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ EVENT_MAP = {
|
|||
'on_cancel': 'After Cancel',
|
||||
'on_trash': 'Before Delete',
|
||||
'after_delete': 'After Delete',
|
||||
'before_update_after_submit': 'Before Save (Submitted Document)',
|
||||
'on_update_after_submit': 'After Save (Submitted Document)'
|
||||
}
|
||||
|
||||
def run_server_script_api(method):
|
||||
|
|
|
|||
|
|
@ -182,6 +182,8 @@ def get_notification_info():
|
|||
return out
|
||||
|
||||
def get_notification_config():
|
||||
user = frappe.session.user or 'Guest'
|
||||
|
||||
def _get():
|
||||
subscribed_documents = get_subscribed_documents()
|
||||
config = frappe._dict()
|
||||
|
|
@ -205,7 +207,7 @@ def get_notification_config():
|
|||
config[key].update(nc.get(key, {}))
|
||||
return config
|
||||
|
||||
return frappe.cache().hget("notification_config", frappe.session.user, _get)
|
||||
return frappe.cache().hget("notification_config", user, _get)
|
||||
|
||||
def get_filters_for(doctype):
|
||||
'''get open filters for doctype'''
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from frappe.desk.doctype.global_search_settings.global_search_settings import up
|
|||
def install():
|
||||
update_genders_and_salutations()
|
||||
update_global_search_doctypes()
|
||||
setup_email_linking()
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_genders_and_salutations():
|
||||
|
|
@ -20,13 +21,12 @@ def update_genders_and_salutations():
|
|||
for record in records:
|
||||
doc = frappe.new_doc(record.get("doctype"))
|
||||
doc.update(record)
|
||||
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
|
||||
try:
|
||||
doc.insert(ignore_permissions=True)
|
||||
except frappe.DuplicateEntryError as e:
|
||||
# pass DuplicateEntryError and continue
|
||||
if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name:
|
||||
# make sure DuplicateEntryError is for the exact same doc and not a related doc
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
def setup_email_linking():
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Email Account",
|
||||
"email_id": "email_linking@example.com",
|
||||
})
|
||||
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
|
||||
|
|
@ -510,7 +510,7 @@ def has_match(row, linked_doctypes, doctype_match_filters, ref_doctype, if_owner
|
|||
cell_value = None
|
||||
if isinstance(row, dict):
|
||||
cell_value = row.get(idx)
|
||||
elif isinstance(row, list):
|
||||
elif isinstance(row, (list, tuple)):
|
||||
cell_value = row[idx]
|
||||
|
||||
if dt in match_filters and cell_value not in match_filters.get(dt) and frappe.db.exists(dt, cell_value):
|
||||
|
|
|
|||
|
|
@ -267,18 +267,14 @@ def get_sidebar_stats(stats, doctype, filters=[]):
|
|||
data = frappe._dict(frappe.local.form_dict)
|
||||
filters = json.loads(data["filters"])
|
||||
|
||||
if not frappe.cache().hget("Tags", doctype):
|
||||
tags = set([tag.tag for tag in frappe.get_list("Tag Link", filters={"document_type": doctype}, fields=["tag"])])
|
||||
frappe.cache().hset("Tags", doctype, tags)
|
||||
|
||||
for tag in list(frappe.cache().hget("Tags", doctype)):
|
||||
for tag in frappe.get_all("Tag Link", filters={"document_type": doctype}, fields=["tag"]):
|
||||
tag_filters = []
|
||||
tag_filters.extend(filters)
|
||||
tag_filters.extend([['Tag Link', 'tag', '=', tag]])
|
||||
tag_filters.extend([['Tag Link', 'tag', '=', tag.tag]])
|
||||
|
||||
count = frappe.get_all(doctype, filters=tag_filters, fields=["count(*)"])
|
||||
if count[0].get("count(*)") > 0:
|
||||
_user_tags.append([tag, count[0].get("count(*)")])
|
||||
_user_tags.append([tag.tag, count[0].get("count(*)")])
|
||||
|
||||
return {"stats": {"_user_tags": _user_tags}}
|
||||
|
||||
|
|
|
|||
|
|
@ -322,16 +322,16 @@ class EmailAccount(Document):
|
|||
unhandled_email.insert(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
|
||||
def insert_communication(self, msg, args={}):
|
||||
def insert_communication(self, msg, args=None):
|
||||
if isinstance(msg, list):
|
||||
raw, uid, seen = msg
|
||||
else:
|
||||
raw = msg
|
||||
uid = -1
|
||||
seen = 0
|
||||
|
||||
if args.get("uid", -1): uid = args.get("uid", -1)
|
||||
if args.get("seen", 0): seen = args.get("seen", 0)
|
||||
if isinstance(args, dict):
|
||||
if args.get("uid", -1): uid = args.get("uid", -1)
|
||||
if args.get("seen", 0): seen = args.get("seen", 0)
|
||||
|
||||
email = Email(raw)
|
||||
|
||||
|
|
@ -355,7 +355,7 @@ class EmailAccount(Document):
|
|||
name = names[0].get("name")
|
||||
# email is already available update communication uid instead
|
||||
frappe.db.set_value("Communication", name, "uid", uid, update_modified=False)
|
||||
return
|
||||
return frappe.get_doc("Communication", name)
|
||||
|
||||
if email.content_type == 'text/html':
|
||||
email.content = clean_email_html(email.content)
|
||||
|
|
|
|||
|
|
@ -3,22 +3,25 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import sys
|
||||
from six.moves import html_parser as HTMLParser
|
||||
import smtplib, quopri, json
|
||||
from frappe import msgprint, throw, _, safe_decode
|
||||
from frappe import msgprint, _, safe_decode
|
||||
from frappe.email.smtp import SMTPServer, get_outgoing_email_account
|
||||
from frappe.email.email_body import get_email, get_formatted_html, add_attachment
|
||||
from frappe.utils.verified_command import get_signed_params, verify_request
|
||||
from html2text import html2text
|
||||
from frappe.utils import get_url, nowdate, encode, now_datetime, add_days, split_emails, cstr, cint
|
||||
from frappe.utils import get_url, nowdate, now_datetime, add_days, split_emails, cstr, cint
|
||||
from rq.timeouts import JobTimeoutException
|
||||
from six import text_type, string_types
|
||||
from six import text_type, string_types, PY3
|
||||
from email.parser import Parser
|
||||
|
||||
|
||||
class EmailLimitCrossedError(frappe.ValidationError): pass
|
||||
|
||||
def send(recipients=None, sender=None, subject=None, message=None, text_content=None, reference_doctype=None,
|
||||
reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None,
|
||||
attachments=None, reply_to=None, cc=[], bcc=[], message_id=None, in_reply_to=None, send_after=None,
|
||||
attachments=None, reply_to=None, cc=None, bcc=None, message_id=None, in_reply_to=None, send_after=None,
|
||||
expose_recipients=None, send_priority=1, communication=None, now=False, read_receipt=None,
|
||||
queue_separately=False, is_notification=False, add_unsubscribe_link=1, inline_images=None,
|
||||
header=None, print_letterhead=False):
|
||||
|
|
@ -52,6 +55,11 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content=
|
|||
if not recipients and not cc:
|
||||
return
|
||||
|
||||
if not cc:
|
||||
cc = []
|
||||
if not bcc:
|
||||
bcc = []
|
||||
|
||||
if isinstance(recipients, string_types):
|
||||
recipients = split_emails(recipients)
|
||||
|
||||
|
|
@ -68,7 +76,6 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content=
|
|||
if not sender or sender == "Administrator":
|
||||
sender = email_account.default_sender
|
||||
|
||||
|
||||
if not text_content:
|
||||
try:
|
||||
text_content = html2text(message)
|
||||
|
|
@ -404,7 +411,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
|
|||
|
||||
message = prepare_message(email, recipient.recipient, recipients_list)
|
||||
if not frappe.flags.in_test:
|
||||
smtpserver.sess.sendmail(email.sender, recipient.recipient, encode(message))
|
||||
smtpserver.sess.sendmail(email.sender, recipient.recipient, message)
|
||||
|
||||
recipient.status = "Sent"
|
||||
frappe.db.sql("""update `tabEmail Queue Recipient` set status='Sent', modified=%s where name=%s""",
|
||||
|
|
@ -509,37 +516,41 @@ def prepare_message(email, recipient, recipients_list):
|
|||
|
||||
message = (message and message.encode('utf8')) or ''
|
||||
message = safe_decode(message)
|
||||
if not email.attachments:
|
||||
return message
|
||||
|
||||
# On-demand attachments
|
||||
from email.parser import Parser
|
||||
if PY3:
|
||||
from email.policy import SMTPUTF8
|
||||
message = Parser(policy=SMTPUTF8).parsestr(message)
|
||||
else:
|
||||
message = Parser().parsestr(message)
|
||||
|
||||
msg_obj = Parser().parsestr(message)
|
||||
attachments = json.loads(email.attachments)
|
||||
if email.attachments:
|
||||
# On-demand attachments
|
||||
|
||||
for attachment in attachments:
|
||||
if attachment.get('fcontent'): continue
|
||||
attachments = json.loads(email.attachments)
|
||||
|
||||
fid = attachment.get("fid")
|
||||
if fid:
|
||||
_file = frappe.get_doc("File", fid)
|
||||
fcontent = _file.get_content()
|
||||
attachment.update({
|
||||
'fname': _file.file_name,
|
||||
'fcontent': fcontent,
|
||||
'parent': msg_obj
|
||||
})
|
||||
attachment.pop("fid", None)
|
||||
add_attachment(**attachment)
|
||||
for attachment in attachments:
|
||||
if attachment.get('fcontent'):
|
||||
continue
|
||||
|
||||
elif attachment.get("print_format_attachment") == 1:
|
||||
attachment.pop("print_format_attachment", None)
|
||||
print_format_file = frappe.attach_print(**attachment)
|
||||
print_format_file.update({"parent": msg_obj})
|
||||
add_attachment(**print_format_file)
|
||||
fid = attachment.get("fid")
|
||||
if fid:
|
||||
_file = frappe.get_doc("File", fid)
|
||||
fcontent = _file.get_content()
|
||||
attachment.update({
|
||||
'fname': _file.file_name,
|
||||
'fcontent': fcontent,
|
||||
'parent': message
|
||||
})
|
||||
attachment.pop("fid", None)
|
||||
add_attachment(**attachment)
|
||||
|
||||
return msg_obj.as_string()
|
||||
elif attachment.get("print_format_attachment") == 1:
|
||||
attachment.pop("print_format_attachment", None)
|
||||
print_format_file = frappe.attach_print(**attachment)
|
||||
print_format_file.update({"parent": message})
|
||||
add_attachment(**print_format_file)
|
||||
|
||||
return message.as_string()
|
||||
|
||||
def clear_outbox():
|
||||
"""Remove low priority older than 31 days in Outbox and expire mails not sent for 7 days.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ from __future__ import unicode_literals
|
|||
import unittest, os, base64
|
||||
from frappe.email.receive import Email
|
||||
from frappe.email.email_body import (replace_filename_with_cid,
|
||||
get_email, inline_style_in_html, get_header)
|
||||
get_email, inline_style_in_html, get_header)
|
||||
from frappe.email.queue import prepare_message, get_email_queue
|
||||
from six import PY3
|
||||
|
||||
|
||||
class TestEmailBody(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
|
@ -37,6 +40,53 @@ This is the text version of this email
|
|||
text_content=email_text
|
||||
).as_string()
|
||||
|
||||
def test_prepare_message_returns_already_encoded_string(self):
|
||||
|
||||
if PY3:
|
||||
uni_chr1 = chr(40960)
|
||||
uni_chr2 = chr(1972)
|
||||
else:
|
||||
uni_chr1 = unichr(40960)
|
||||
uni_chr2 = unichr(1972)
|
||||
|
||||
email = get_email_queue(
|
||||
recipients=['test@example.com'],
|
||||
sender='me@example.com',
|
||||
subject='Test Subject',
|
||||
content='<h1>' + uni_chr1 + 'abcd' + uni_chr2 + '</h1>',
|
||||
formatted='<h1>' + uni_chr1 + 'abcd' + uni_chr2 + '</h1>',
|
||||
text_content='whatever')
|
||||
result = prepare_message(email=email, recipient='test@test.com', recipients_list=[])
|
||||
self.assertTrue("<h1>=EA=80=80abcd=DE=B4</h1>" in result)
|
||||
|
||||
def test_prepare_message_returns_cr_lf(self):
|
||||
email = get_email_queue(
|
||||
recipients=['test@example.com'],
|
||||
sender='me@example.com',
|
||||
subject='Test Subject',
|
||||
content='<h1>\n this is a test of newlines\n' + '</h1>',
|
||||
formatted='<h1>\n this is a test of newlines\n' + '</h1>',
|
||||
text_content='whatever')
|
||||
result = prepare_message(email=email, recipient='test@test.com', recipients_list=[])
|
||||
if PY3:
|
||||
self.assertTrue(result.count('\n') == result.count("\r"))
|
||||
else:
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_rfc_5322_header_is_wrapped_at_998_chars(self):
|
||||
# unfortunately the db can only hold 140 chars so this can't be tested properly. test at max chars anyway.
|
||||
email = get_email_queue(
|
||||
recipients=['test@example.com'],
|
||||
sender='me@example.com',
|
||||
subject='Test Subject',
|
||||
content='<h1>Whatever</h1>',
|
||||
text_content='whatever',
|
||||
message_id= "a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" +
|
||||
".really.long.message.id.that.should.not.wrap.unti")
|
||||
result = prepare_message(email=email, recipient='test@test.com', recipients_list=[])
|
||||
self.assertTrue(
|
||||
"a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" +
|
||||
".really.long.message.id.that.should.not.wrap.unti" in result)
|
||||
|
||||
def test_image(self):
|
||||
img_signature = '''
|
||||
|
|
@ -49,7 +99,6 @@ Content-Disposition: inline; filename="favicon.png"
|
|||
self.assertTrue(img_signature in self.email_string)
|
||||
self.assertTrue(self.img_base64 in self.email_string)
|
||||
|
||||
|
||||
def test_text_content(self):
|
||||
text_content = '''
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
|
|
@ -62,7 +111,6 @@ This is the text version of this email
|
|||
'''
|
||||
self.assertTrue(text_content in self.email_string)
|
||||
|
||||
|
||||
def test_email_content(self):
|
||||
html_head = '''
|
||||
Content-Type: text/html; charset="utf-8"
|
||||
|
|
@ -79,7 +127,6 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|||
self.assertTrue(html_head in self.email_string)
|
||||
self.assertTrue(html in self.email_string)
|
||||
|
||||
|
||||
def test_replace_filename_with_cid(self):
|
||||
original_message = '''
|
||||
<div>
|
||||
|
|
@ -152,6 +199,7 @@ Reply-To: test2_@erpnext.com
|
|||
mail = Email(content_bytes)
|
||||
self.assertEqual(mail.text_content, text_content)
|
||||
|
||||
|
||||
def fixed_column_width(string, chunk_size):
|
||||
parts = [string[0+i:chunk_size+i] for i in range(0, len(string), chunk_size)]
|
||||
return '\n'.join(parts)
|
||||
parts = [string[0 + i:chunk_size + i] for i in range(0, len(string), chunk_size)]
|
||||
return '\n'.join(parts)
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@
|
|||
"label": "User ID Property"
|
||||
}
|
||||
],
|
||||
"modified": "2019-12-03 12:35:55.115260",
|
||||
"modified": "2019-12-03 13:13:46.989099",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Social Login Key",
|
||||
|
|
|
|||
|
|
@ -61,7 +61,9 @@ def set_user_and_static_default_values(doc):
|
|||
|
||||
user_default_value = get_user_default_value(df, defaults, doctype_user_permissions, allowed_records, default_doc)
|
||||
if user_default_value is not None:
|
||||
doc.set(df.fieldname, user_default_value)
|
||||
# if fieldtype is link check if doc exists
|
||||
if not df.fieldtype == "Link" or frappe.db.exists(df.options, user_default_value):
|
||||
doc.set(df.fieldname, user_default_value)
|
||||
|
||||
else:
|
||||
if df.fieldname != doc.meta.title_field:
|
||||
|
|
|
|||
|
|
@ -260,4 +260,6 @@ frappe.patches.v12_0.update_auto_repeat_status_and_not_submittable
|
|||
frappe.patches.v12_0.copy_to_parent_for_tags
|
||||
frappe.patches.v12_0.create_notification_settings_for_user
|
||||
frappe.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26
|
||||
frappe.patches.v12_0.setup_email_linking
|
||||
frappe.patches.v12_0.fix_home_settings_for_all_users
|
||||
execute:frappe.delete_doc("Test Runner")
|
||||
41
frappe/patches/v12_0/fix_home_settings_for_all_users.py
Normal file
41
frappe/patches/v12_0/fix_home_settings_for_all_users.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import frappe
|
||||
from frappe.config import get_modules_from_all_apps_for_user
|
||||
import json
|
||||
def execute():
|
||||
users = frappe.get_all('User', fields=['name', 'home_settings'])
|
||||
|
||||
for user in users:
|
||||
|
||||
if not user.home_settings:
|
||||
continue
|
||||
|
||||
home_settings = json.loads(user.home_settings)
|
||||
|
||||
modules_by_category = home_settings.get('modules_by_category')
|
||||
if not modules_by_category:
|
||||
continue
|
||||
visible_modules = []
|
||||
category_to_check = []
|
||||
|
||||
for category, modules in modules_by_category.items():
|
||||
visible_modules += modules
|
||||
category_to_check.append(category)
|
||||
|
||||
all_modules = get_modules_from_all_apps_for_user(user.name)
|
||||
all_modules = set([m.get('name') or m.get('module_name') or m.get('label') \
|
||||
for m in all_modules if m.get('category') in category_to_check])
|
||||
|
||||
hidden_modules = home_settings.get("hidden_modules", [])
|
||||
|
||||
modules_in_home_settings = set(visible_modules + hidden_modules)
|
||||
|
||||
all_modules = all_modules.union(modules_in_home_settings)
|
||||
|
||||
missing_modules = all_modules - modules_in_home_settings
|
||||
|
||||
if missing_modules:
|
||||
home_settings['hidden_modules'] = hidden_modules + list(missing_modules)
|
||||
home_settings = json.dumps(home_settings)
|
||||
frappe.set_value('User', user.name, 'home_settings', home_settings)
|
||||
|
||||
frappe.cache().delete_key('home_settings')
|
||||
6
frappe/patches/v12_0/setup_email_linking.py
Normal file
6
frappe/patches/v12_0/setup_email_linking.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from frappe.desk.page.setup_wizard.install_fixtures import setup_email_linking
|
||||
|
||||
def execute():
|
||||
setup_email_linking()
|
||||
|
|
@ -12,9 +12,10 @@
|
|||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"image_src": "/assets/erpnext/images/illustrations/letterhead-onboard.png",
|
||||
"image_src": "",
|
||||
"is_completed": 1,
|
||||
"max_count": 0,
|
||||
"modified": "2019-12-03 22:54:57.618989",
|
||||
"modified": "2019-12-09 15:12:45.588567",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Company Letter Head",
|
||||
"owner": "Administrator",
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@
|
|||
{{ app_info.title }}
|
||||
<small>{{ __("updated to {0}", [app_info.version]) }}</small>
|
||||
</h2>
|
||||
<div class="app-change-log-body">
|
||||
{% for (var x=0, y=app_info.change_log.length; x < y; x++) {
|
||||
var version_info = app_info.change_log[x];
|
||||
if(version_info) { %}
|
||||
<p>{{ frappe.markdown(version_info[1]) }}</p>
|
||||
{% }
|
||||
} %}
|
||||
</div>
|
||||
</div>
|
||||
{% } %}
|
||||
|
|
|
|||
|
|
@ -466,12 +466,27 @@ frappe.Application = Class.extend({
|
|||
|
||||
show_change_log: function() {
|
||||
var me = this;
|
||||
var d = frappe.msgprint(
|
||||
frappe.render_template("change_log", {"change_log": frappe.boot.change_log}),
|
||||
__("Updated To New Version")
|
||||
);
|
||||
d.keep_open = true;
|
||||
d.custom_onhide = function() {
|
||||
let change_log = frappe.boot.change_log;
|
||||
|
||||
// frappe.boot.change_log = [{
|
||||
// "change_log": [
|
||||
// [<version>, <change_log in markdown>],
|
||||
// [<version>, <change_log in markdown>],
|
||||
// ],
|
||||
// "description": "ERP made simple",
|
||||
// "title": "ERPNext",
|
||||
// "version": "12.2.0"
|
||||
// }];
|
||||
|
||||
// Iterate over changelog
|
||||
var change_log_dialog = frappe.msgprint({
|
||||
message: frappe.render_template("change_log", {"change_log": change_log}),
|
||||
title: __("Updated To New Version 🎉"),
|
||||
wide: true,
|
||||
scroll: true
|
||||
});
|
||||
change_log_dialog.keep_open = true;
|
||||
change_log_dialog.custom_onhide = function() {
|
||||
frappe.call({
|
||||
"method": "frappe.utils.change_log.update_last_known_versions"
|
||||
});
|
||||
|
|
|
|||
|
|
@ -291,7 +291,7 @@ frappe.get_modal = function(title, content) {
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="flex justify-between">
|
||||
<div class="fill-width">
|
||||
<div class="fill-width flex">
|
||||
<span class="indicator hidden"></span>
|
||||
<h4 class="modal-title" style="font-weight: bold;">${title}</h4>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({
|
|||
},
|
||||
eval_expression: function(value) {
|
||||
if (typeof value === 'string') {
|
||||
if (value.match(/^[0-9\+\-\/\* ]+$/)) {
|
||||
if (value.match(/^[0-9+\-/* ]+$/)) {
|
||||
// If it is a string containing operators
|
||||
try {
|
||||
return eval(value);
|
||||
|
|
|
|||
|
|
@ -195,11 +195,14 @@ export default class Grid {
|
|||
}
|
||||
|
||||
delete_all_rows() {
|
||||
this.frm.doc[this.df.fieldname] = [];
|
||||
$(this.parent).find('.rows').empty();
|
||||
this.grid_rows = [];
|
||||
this.refresh();
|
||||
frappe.utils.scroll_to(this.wrapper);
|
||||
frappe.confirm(__("Are you sure you want to delete all rows?"), () => {
|
||||
this.frm.doc[this.df.fieldname] = [];
|
||||
$(this.parent).find('.rows').empty();
|
||||
this.grid_rows = [];
|
||||
this.refresh();
|
||||
frappe.utils.scroll_to(this.wrapper);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
select_row(name) {
|
||||
|
|
@ -304,11 +307,12 @@ export default class Grid {
|
|||
|
||||
|
||||
render_result_rows($rows, append_row) {
|
||||
|
||||
let result_length = this.grid_pagination.get_result_length();
|
||||
let page_index = this.grid_pagination.page_index;
|
||||
let page_length = this.grid_pagination.page_length;
|
||||
|
||||
if (!this.grid_rows) {
|
||||
return;
|
||||
}
|
||||
for (var ri = (page_index-1)*page_length; ri < result_length; ri++) {
|
||||
var d = this.data[ri];
|
||||
if (!d) {
|
||||
|
|
@ -364,9 +368,9 @@ export default class Grid {
|
|||
truncate_rows() {
|
||||
if (this.grid_rows.length > this.data.length) {
|
||||
// remove extra rows
|
||||
for (var i=this.data.length; i < this.grid_rows.length; i++) {
|
||||
for (var i = this.data.length; i < this.grid_rows.length; i++) {
|
||||
var grid_row = this.grid_rows[i];
|
||||
grid_row.wrapper.remove();
|
||||
if (grid_row) grid_row.wrapper.remove();
|
||||
}
|
||||
this.grid_rows.splice(this.data.length);
|
||||
}
|
||||
|
|
@ -755,6 +759,7 @@ export default class Grid {
|
|||
}
|
||||
|
||||
setup_allow_bulk_edit() {
|
||||
let me = this;
|
||||
if (this.frm && this.frm.get_docfield(this.df.fieldname).allow_bulk_edit) {
|
||||
// download
|
||||
this.setup_download();
|
||||
|
|
@ -769,8 +774,7 @@ export default class Grid {
|
|||
var data = frappe.utils.csv_to_array(frappe.utils.get_decoded_string(file.dataurl));
|
||||
// row #2 contains fieldnames;
|
||||
var fieldnames = data[2];
|
||||
|
||||
this.frm.clear_table(this.df.fieldname);
|
||||
me.frm.clear_table(me.df.fieldname);
|
||||
$.each(data, (i, row) => {
|
||||
if (i > 6) {
|
||||
var blank_row = true;
|
||||
|
|
@ -782,10 +786,10 @@ export default class Grid {
|
|||
});
|
||||
|
||||
if (!blank_row) {
|
||||
var d = this.frm.add_child(this.df.fieldname);
|
||||
var d = me.frm.add_child(me.df.fieldname);
|
||||
$.each(row, (ci, value) => {
|
||||
var fieldname = fieldnames[ci];
|
||||
var df = frappe.meta.get_docfield(this.df.options, fieldname);
|
||||
var df = frappe.meta.get_docfield(me.df.options, fieldname);
|
||||
|
||||
// convert date formatting
|
||||
if (df.fieldtype==="Date" && value) {
|
||||
|
|
@ -802,7 +806,7 @@ export default class Grid {
|
|||
}
|
||||
});
|
||||
|
||||
this.frm.refresh_field(this.df.fieldname);
|
||||
me.frm.refresh_field(me.df.fieldname);
|
||||
frappe.msgprint({message: __('Table updated'), title: __('Success'), indicator: 'green'});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export default class GridPagination {
|
|||
}
|
||||
|
||||
setup_pagination() {
|
||||
this.page_length = 20;
|
||||
this.page_length = 50;
|
||||
this.page_index = 1;
|
||||
this.total_pages = Math.ceil(this.grid.data.length/this.page_length);
|
||||
|
||||
|
|
|
|||
|
|
@ -41,40 +41,45 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
|
|||
};
|
||||
|
||||
var remove_empty_rows = function() {
|
||||
/**
|
||||
This function removes empty rows. Note that in this function, a row is considered
|
||||
empty if the fields with `in_list_view: 1` are undefined or falsy because that's
|
||||
what users also consider to be an empty row
|
||||
*/
|
||||
/*
|
||||
This function removes empty rows. Note that in this function, a row is considered
|
||||
empty if the fields with `in_list_view: 1` are undefined or falsy because that's
|
||||
what users also consider to be an empty row
|
||||
*/
|
||||
const docs = frappe.model.get_all_docs(frm.doc);
|
||||
|
||||
// we should only worry about table data
|
||||
const tables = docs.filter(function(d){
|
||||
const tables = docs.filter(d => {
|
||||
return frappe.model.is_table(d.doctype);
|
||||
});
|
||||
|
||||
tables.map(
|
||||
function(doc){
|
||||
const cells = frappe.meta.docfield_list[doc.doctype] || [];
|
||||
let modified_table_fields = [];
|
||||
|
||||
const in_list_view_cells = cells.filter(function(df) {
|
||||
return cint(df.in_list_view) === 1;
|
||||
});
|
||||
tables.map(doc => {
|
||||
const cells = frappe.meta.docfield_list[doc.doctype] || [];
|
||||
|
||||
var is_empty_row = function(cells) {
|
||||
for (var i=0; i < cells.length; i++){
|
||||
if(locals[doc.doctype][doc.name][cells[i].fieldname]){
|
||||
return false;
|
||||
}
|
||||
const in_list_view_cells = cells.filter((df) => {
|
||||
return cint(df.in_list_view) === 1;
|
||||
});
|
||||
|
||||
const is_empty_row = function(cells) {
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
if (locals[doc.doctype][doc.name][cells[i].fieldname]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (is_empty_row(in_list_view_cells)) {
|
||||
frappe.model.clear_doc(doc.doctype, doc.name);
|
||||
}
|
||||
if (is_empty_row(in_list_view_cells)) {
|
||||
frappe.model.clear_doc(doc.doctype, doc.name);
|
||||
modified_table_fields.push(doc.parentfield);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
modified_table_fields.forEach(field => {
|
||||
frm.refresh_field(field);
|
||||
});
|
||||
};
|
||||
|
||||
var cancel = function () {
|
||||
|
|
|
|||
|
|
@ -88,7 +88,10 @@ frappe.ui.form.setup_user_image_event = function(frm) {
|
|||
}
|
||||
field.$input.trigger('click');
|
||||
} else {
|
||||
field.set_value('').then(() => frm.save());
|
||||
/// on remove event for a sidebar image wrapper remove attach file.
|
||||
frm.attachments.remove_attachment_by_filename(frm.doc[frm.meta.image_field], function() {
|
||||
field.set_value('').then(() => frm.save());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ frappe.views.BaseList = class BaseList {
|
|||
|
||||
show_or_hide_sidebar() {
|
||||
let show_sidebar = JSON.parse(localStorage.show_sidebar || 'true');
|
||||
$(document.body).toggleClass('no-sidebar', !show_sidebar);
|
||||
$(document.body).toggleClass('no-list-sidebar', !show_sidebar);
|
||||
}
|
||||
|
||||
setup_main_section() {
|
||||
|
|
@ -637,7 +637,8 @@ class FilterArea {
|
|||
condition: condition,
|
||||
default: default_value,
|
||||
onchange: () => this.refresh_list_view(),
|
||||
ignore_link_validation: fieldtype === 'Dynamic Link'
|
||||
ignore_link_validation: fieldtype === 'Dynamic Link',
|
||||
is_filter: 1,
|
||||
};
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -40,8 +40,7 @@ frappe.views.ListSidebar = class ListSidebar {
|
|||
this.sidebar.find('.sidebar-stat').remove();
|
||||
} else {
|
||||
this.sidebar.find('.list-stats').on('click', (e) => {
|
||||
$(e.currentTarget).find('.stat-link').remove();
|
||||
this.get_stats();
|
||||
this.reload_stats();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -253,13 +252,15 @@ frappe.views.ListSidebar = class ListSidebar {
|
|||
let text_filter = $search_input.val().toLowerCase();
|
||||
// Replace trailing and leading spaces
|
||||
text_filter = text_filter.replace(/^\s+|\s+$/g, '');
|
||||
let text;
|
||||
for (var i = 0; i < $elements.length; i++) {
|
||||
let text_element = $elements.eq(i).find(text_class);
|
||||
|
||||
let text = text_element.text().toLowerCase();
|
||||
// Search data-name since label for current user is 'Me'
|
||||
let name = text_element.data('name').toLowerCase();
|
||||
let name = '';
|
||||
if (text_element.data('name')) {
|
||||
name = text_element.data('name').toLowerCase();
|
||||
}
|
||||
if (text.includes(text_filter) || name.includes(text_filter)) {
|
||||
$elements.eq(i).css('display','');
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -137,8 +137,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
|
||||
show_restricted_list_indicator_if_applicable() {
|
||||
const match_rules_list = frappe.perm.get_match_rules(this.doctype);
|
||||
if(match_rules_list.length) {
|
||||
this.restricted_list = $('<button class="restricted-list form-group">Restricted</button>')
|
||||
if (match_rules_list.length) {
|
||||
this.restricted_list = $(`<button class="restricted-list form-group">${__('Restricted')}</button>`)
|
||||
.prepend('<span class="octicon octicon-lock"></span>')
|
||||
.click(() => this.show_restrictions(match_rules_list))
|
||||
.appendTo(this.page.page_form);
|
||||
|
|
@ -148,7 +148,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
show_restrictions(match_rules_list=[]) {
|
||||
frappe.msgprint(frappe.render_template('list_view_permission_restrictions', {
|
||||
condition_list: match_rules_list
|
||||
}), 'Restrictions');
|
||||
}), __('Restrictions'));
|
||||
}
|
||||
|
||||
set_fields() {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,12 @@ frappe.ui.FilterGroup = class {
|
|||
}
|
||||
|
||||
validate_args(doctype, fieldname) {
|
||||
// Tags attached to the document are maintained seperately in Tag Link
|
||||
// and is not the part of doctype meta therefore tag fieldname validation is ignored.
|
||||
if (doctype === "Tag Link" && fieldname === "tag") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(doctype && fieldname
|
||||
&& !frappe.meta.has_field(doctype, fieldname)
|
||||
&& !frappe.model.std_fields_list.includes(fieldname)) {
|
||||
|
|
|
|||
|
|
@ -207,6 +207,15 @@ frappe.msgprint = function(msg, title) {
|
|||
frappe.msg_dialog.wrapper.classList.add('msgprint-dialog');
|
||||
}
|
||||
|
||||
if (data.scroll) {
|
||||
// limit modal height and allow scrolling instead
|
||||
frappe.msg_dialog.body.classList.add('msgprint-scroll');
|
||||
} else {
|
||||
if (frappe.msg_dialog.body.classList.contains('msgprint-scroll')) {
|
||||
frappe.msg_dialog.body.classList.remove('msgprint-scroll');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(msg_exists) {
|
||||
frappe.msg_dialog.msg_area.append("<hr>");
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ function format_currency(v, currency, decimals) {
|
|||
|
||||
function get_currency_symbol(currency) {
|
||||
if (frappe.boot) {
|
||||
if (frappe.boot.sysdefaults.hide_currency_symbol == "Yes")
|
||||
if (frappe.boot.sysdefaults && frappe.boot.sysdefaults.hide_currency_symbol == "Yes")
|
||||
return null;
|
||||
|
||||
if (!currency)
|
||||
|
|
|
|||
|
|
@ -452,6 +452,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
|
||||
render_datatable() {
|
||||
let data = this.data;
|
||||
let columns = this.columns.filter((col) => !col.hidden);
|
||||
|
||||
if (this.raw_data.add_total_row) {
|
||||
data = data.slice();
|
||||
|
|
@ -460,10 +461,10 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
|
||||
if (this.datatable) {
|
||||
this.datatable.options.treeView = this.tree_report;
|
||||
this.datatable.refresh(data, this.columns);
|
||||
this.datatable.refresh(data, columns);
|
||||
} else {
|
||||
let datatable_options = {
|
||||
columns: this.columns.filter((col) => !col.hidden),
|
||||
columns: columns,
|
||||
data: data,
|
||||
inlineFilters: true,
|
||||
treeView: this.tree_report,
|
||||
|
|
@ -965,8 +966,10 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
}
|
||||
|
||||
get_data_for_csv(include_indentation) {
|
||||
const indices = this.datatable.bodyRenderer.visibleRowIndices;
|
||||
const rows = indices.map(i => this.datatable.datamanager.getRow(i));
|
||||
const rows = this.datatable.bodyRenderer.visibleRows;
|
||||
if (this.raw_data.add_total_row) {
|
||||
rows.push(this.datatable.bodyRenderer.getTotalRow());
|
||||
}
|
||||
return rows.map(row => {
|
||||
const standard_column_count = this.datatable.datamanager.getStandardColumnCount();
|
||||
return row
|
||||
|
|
|
|||
|
|
@ -615,15 +615,18 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
}
|
||||
|
||||
is_editable(df, data) {
|
||||
if (!df || data.docstatus !== 0) return false;
|
||||
const is_standard_field = frappe.model.std_fields_list.includes(df.fieldname);
|
||||
const can_edit = !(
|
||||
is_standard_field
|
||||
|| df.read_only
|
||||
|| df.hidden
|
||||
|| !frappe.model.can_write(this.doctype)
|
||||
);
|
||||
return can_edit;
|
||||
if (df
|
||||
&& frappe.model.can_write(this.doctype)
|
||||
// not a submitted doc or field is allowed to edit after submit
|
||||
&& (data.docstatus !== 1 || df.allow_on_submit)
|
||||
// not a cancelled doc
|
||||
&& data.docstatus !== 2
|
||||
&& !df.read_only
|
||||
&& !df.hidden
|
||||
// not a standard field i.e., owner, modified_by, etc.
|
||||
&& !frappe.model.std_fields_list.includes(df.fieldname))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
get_data(values) {
|
||||
|
|
|
|||
|
|
@ -157,6 +157,11 @@ a.badge-hover& {
|
|||
}
|
||||
}
|
||||
|
||||
.msgprint-scroll {
|
||||
max-height: 36em;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.msgprint {
|
||||
// margin: 15px 0px;
|
||||
// text-align: center;
|
||||
|
|
|
|||
|
|
@ -771,7 +771,7 @@ li.user-progress {
|
|||
// custom font awesome checkbox
|
||||
input[type="checkbox"] {
|
||||
position: relative;
|
||||
left: -999999px;
|
||||
visibility: hidden;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
|
|
@ -787,7 +787,7 @@ input[type="checkbox"] {
|
|||
font-size: 14px;
|
||||
color: @text-extra-muted;
|
||||
.transition(150ms color);
|
||||
left: 999999px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
&:focus:before {
|
||||
|
|
@ -970,8 +970,20 @@ input[type="checkbox"] {
|
|||
margin-left: auto;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border: 1px solid #d1d8dd;
|
||||
box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.modal-backdrop.in {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
width: 50%;
|
||||
height: 80%;
|
||||
max-width: none;
|
||||
}
|
||||
|
|
@ -1137,3 +1149,9 @@ body.no-sidebar {
|
|||
.alt-pressed .alt-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.app-change-log-body {
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -262,6 +262,14 @@
|
|||
border-bottom: 1px solid @border-color;
|
||||
}
|
||||
|
||||
.grid-header-toolbar {
|
||||
display: flow-root;
|
||||
}
|
||||
|
||||
.grid-buttons {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.grid-footer {
|
||||
background-color: #fff;
|
||||
border: 1px solid @border-color;
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@
|
|||
}
|
||||
|
||||
.modal-header .indicator {
|
||||
float: left;
|
||||
margin-top: 7.5px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
body.no-list-sidebar {
|
||||
[data-page-route^="List/"] {
|
||||
@media (min-width: @screen-md) {
|
||||
.layout-side-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.layout-main-section-wrapper {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-list {
|
||||
position: relative;
|
||||
|
|
@ -293,9 +306,8 @@ input.list-check-all, input.list-row-checkbox {
|
|||
border-radius: 5px;
|
||||
background: lightyellow;
|
||||
color: @text-light;
|
||||
margin-left: auto;
|
||||
margin: auto 5px auto auto;
|
||||
font-size: @text-small;
|
||||
margin-top: 3px;
|
||||
outline: 0;
|
||||
.octicon {
|
||||
padding-right: 5px;
|
||||
|
|
@ -304,6 +316,13 @@ input.list-check-all, input.list-row-checkbox {
|
|||
}
|
||||
}
|
||||
|
||||
.frappe-rtl {
|
||||
.restricted-list {
|
||||
margin: auto auto auto 5px;
|
||||
direction: ltr;
|
||||
}
|
||||
}
|
||||
|
||||
.taggle_input {
|
||||
padding: 0;
|
||||
margin-top: 3px;
|
||||
|
|
|
|||
|
|
@ -124,17 +124,16 @@
|
|||
}
|
||||
|
||||
.page-form {
|
||||
margin: 0px;
|
||||
padding-right: 15px;
|
||||
padding-top: 10px;
|
||||
margin: 0;
|
||||
padding: 5px 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
border-bottom: 1px solid @border-color;
|
||||
background-color: @panel-bg;
|
||||
|
||||
.form-group {
|
||||
padding-right: 0px;
|
||||
margin-bottom: 10px;
|
||||
padding: 0px;
|
||||
margin: 5px;
|
||||
}
|
||||
.checkbox {
|
||||
margin-top: 4px;
|
||||
|
|
|
|||
|
|
@ -406,6 +406,10 @@ body[data-route^="Module"] .main-menu {
|
|||
.dropdown-search {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.stat-no-records {
|
||||
margin: 5px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// module sidebar
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import unittest, frappe, re, email
|
||||
from six import PY3
|
||||
|
||||
from frappe.test_runner import make_test_records
|
||||
|
||||
make_test_records("User")
|
||||
make_test_records("Email Account")
|
||||
|
||||
|
||||
class TestEmail(unittest.TestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql("""delete from `tabEmail Unsubscribe`""")
|
||||
|
|
@ -16,11 +19,11 @@ class TestEmail(unittest.TestCase):
|
|||
frappe.db.sql("""delete from `tabEmail Queue Recipient`""")
|
||||
|
||||
def test_email_queue(self, send_after=None):
|
||||
frappe.sendmail(recipients = ['test@example.com', 'test1@example.com'],
|
||||
sender="admin@example.com",
|
||||
reference_doctype='User', reference_name='Administrator',
|
||||
subject='Testing Queue', message='This mail is queued!',
|
||||
unsubscribe_message="Unsubscribe", send_after=send_after)
|
||||
frappe.sendmail(recipients=['test@example.com', 'test1@example.com'],
|
||||
sender="admin@example.com",
|
||||
reference_doctype='User', reference_name='Administrator',
|
||||
subject='Testing Queue', message='This mail is queued!',
|
||||
unsubscribe_message="Unsubscribe", send_after=send_after)
|
||||
|
||||
email_queue = frappe.db.sql("""select name,message from `tabEmail Queue` where status='Not Sent'""", as_dict=1)
|
||||
self.assertEqual(len(email_queue), 1)
|
||||
|
|
@ -32,7 +35,7 @@ class TestEmail(unittest.TestCase):
|
|||
self.assertTrue('<!--unsubscribe url-->' in email_queue[0]['message'])
|
||||
|
||||
def test_send_after(self):
|
||||
self.test_email_queue(send_after = 1)
|
||||
self.test_email_queue(send_after=1)
|
||||
from frappe.email.queue import flush
|
||||
flush(from_test=True)
|
||||
email_queue = frappe.db.sql("""select name from `tabEmail Queue` where status='Sent'""", as_dict=1)
|
||||
|
|
@ -52,12 +55,13 @@ class TestEmail(unittest.TestCase):
|
|||
self.assertTrue('Unsubscribe' in frappe.safe_decode(frappe.flags.sent_mail))
|
||||
|
||||
def test_cc_header(self):
|
||||
#test if sending with cc's makes it into header
|
||||
# test if sending with cc's makes it into header
|
||||
frappe.sendmail(recipients=['test@example.com'],
|
||||
cc=['test1@example.com'],
|
||||
sender="admin@example.com",
|
||||
reference_doctype='User', reference_name="Administrator",
|
||||
subject='Testing Email Queue', message='This is mail is queued!', unsubscribe_message="Unsubscribe", expose_recipients="header")
|
||||
cc=['test1@example.com'],
|
||||
sender="admin@example.com",
|
||||
reference_doctype='User', reference_name="Administrator",
|
||||
subject='Testing Email Queue', message='This is mail is queued!',
|
||||
unsubscribe_message="Unsubscribe", expose_recipients="header")
|
||||
email_queue = frappe.db.sql("""select name from `tabEmail Queue` where status='Not Sent'""", as_dict=1)
|
||||
self.assertEqual(len(email_queue), 1)
|
||||
queue_recipients = [r.recipient for r in frappe.db.sql("""select recipient from `tabEmail Queue Recipient`
|
||||
|
|
@ -71,12 +75,13 @@ class TestEmail(unittest.TestCase):
|
|||
self.assertTrue('CC: test1@example.com' in message)
|
||||
|
||||
def test_cc_footer(self):
|
||||
#test if sending with cc's makes it into header
|
||||
# test if sending with cc's makes it into header
|
||||
frappe.sendmail(recipients=['test@example.com'],
|
||||
cc=['test1@example.com'],
|
||||
sender="admin@example.com",
|
||||
reference_doctype='User', reference_name="Administrator",
|
||||
subject='Testing Email Queue', message='This is mail is queued!', unsubscribe_message="Unsubscribe", expose_recipients="footer", now=True)
|
||||
cc=['test1@example.com'],
|
||||
sender="admin@example.com",
|
||||
reference_doctype='User', reference_name="Administrator",
|
||||
subject='Testing Email Queue', message='This is mail is queued!',
|
||||
unsubscribe_message="Unsubscribe", expose_recipients="footer", now=True)
|
||||
email_queue = frappe.db.sql("""select name from `tabEmail Queue` where status='Sent'""", as_dict=1)
|
||||
self.assertEqual(len(email_queue), 1)
|
||||
queue_recipients = [r.recipient for r in frappe.db.sql("""select recipient from `tabEmail Queue Recipient`
|
||||
|
|
@ -84,15 +89,17 @@ class TestEmail(unittest.TestCase):
|
|||
self.assertTrue('test@example.com' in queue_recipients)
|
||||
self.assertTrue('test1@example.com' in queue_recipients)
|
||||
|
||||
self.assertTrue('This email was sent to test@example.com and copied to test1@example.com' in frappe.safe_decode(frappe.flags.sent_mail))
|
||||
self.assertTrue('This email was sent to test@example.com and copied to test1@example.com' in frappe.safe_decode(
|
||||
frappe.flags.sent_mail))
|
||||
|
||||
def test_expose(self):
|
||||
from frappe.utils.verified_command import verify_request
|
||||
frappe.sendmail(recipients=['test@example.com'],
|
||||
cc=['test1@example.com'],
|
||||
sender="admin@example.com",
|
||||
reference_doctype='User', reference_name="Administrator",
|
||||
subject='Testing Email Queue', message='This is mail is queued!', unsubscribe_message="Unsubscribe", now=True)
|
||||
cc=['test1@example.com'],
|
||||
sender="admin@example.com",
|
||||
reference_doctype='User', reference_name="Administrator",
|
||||
subject='Testing Email Queue', message='This is mail is queued!',
|
||||
unsubscribe_message="Unsubscribe", now=True)
|
||||
email_queue = frappe.db.sql("""select name from `tabEmail Queue` where status='Sent'""", as_dict=1)
|
||||
self.assertEqual(len(email_queue), 1)
|
||||
queue_recipients = [r.recipient for r in frappe.db.sql("""select recipient from `tabEmail Queue Recipient`
|
||||
|
|
@ -109,7 +116,14 @@ class TestEmail(unittest.TestCase):
|
|||
content = part.get_payload(decode=True)
|
||||
|
||||
if content:
|
||||
frappe.local.flags.signed_query_string = re.search(r'(?<=/api/method/frappe.email.queue.unsubscribe\?).*(?=\n)', content.decode()).group(0)
|
||||
if PY3:
|
||||
eol = "\r\n"
|
||||
else:
|
||||
eol = "\n"
|
||||
|
||||
frappe.local.flags.signed_query_string = \
|
||||
re.search(r'(?<=/api/method/frappe.email.queue.unsubscribe\?).*(?=' + eol + ')',
|
||||
content.decode()).group(0)
|
||||
self.assertTrue(verify_request())
|
||||
break
|
||||
|
||||
|
|
@ -121,7 +135,7 @@ class TestEmail(unittest.TestCase):
|
|||
email_queue = frappe.db.sql("""select name from `tabEmail Queue` where status='Expired'""", as_dict=1)
|
||||
self.assertEqual(len(email_queue), 1)
|
||||
queue_recipients = [r.recipient for r in frappe.db.sql("""select recipient from `tabEmail Queue Recipient`
|
||||
where parent = %s""",email_queue[0].name, as_dict=1)]
|
||||
where parent = %s""", email_queue[0].name, as_dict=1)]
|
||||
self.assertTrue('test@example.com' in queue_recipients)
|
||||
self.assertTrue('test1@example.com' in queue_recipients)
|
||||
self.assertEqual(len(queue_recipients), 2)
|
||||
|
|
@ -131,19 +145,20 @@ class TestEmail(unittest.TestCase):
|
|||
unsubscribe(doctype="User", name="Administrator", email="test@example.com")
|
||||
|
||||
self.assertTrue(frappe.db.get_value("Email Unsubscribe",
|
||||
{"reference_doctype": "User", "reference_name": "Administrator", "email": "test@example.com"}))
|
||||
{"reference_doctype": "User", "reference_name": "Administrator",
|
||||
"email": "test@example.com"}))
|
||||
|
||||
before = frappe.db.sql("""select count(name) from `tabEmail Queue` where status='Not Sent'""")[0][0]
|
||||
|
||||
send(recipients = ['test@example.com', 'test1@example.com'],
|
||||
sender="admin@example.com",
|
||||
reference_doctype='User', reference_name= "Administrator",
|
||||
subject='Testing Email Queue', message='This is mail is queued!', unsubscribe_message="Unsubscribe")
|
||||
send(recipients=['test@example.com', 'test1@example.com'],
|
||||
sender="admin@example.com",
|
||||
reference_doctype='User', reference_name="Administrator",
|
||||
subject='Testing Email Queue', message='This is mail is queued!', unsubscribe_message="Unsubscribe")
|
||||
|
||||
# this is sent async (?)
|
||||
|
||||
email_queue = frappe.db.sql("""select name from `tabEmail Queue` where status='Not Sent'""",
|
||||
as_dict=1)
|
||||
as_dict=1)
|
||||
self.assertEqual(len(email_queue), before + 1)
|
||||
queue_recipients = [r.recipient for r in frappe.db.sql("""select recipient from `tabEmail Queue Recipient`
|
||||
where status='Not Sent'""", as_dict=1)]
|
||||
|
|
@ -152,7 +167,6 @@ class TestEmail(unittest.TestCase):
|
|||
self.assertEqual(len(queue_recipients), 1)
|
||||
self.assertTrue('Unsubscribe' in frappe.safe_decode(frappe.flags.sent_mail))
|
||||
|
||||
|
||||
def test_image_parsing(self):
|
||||
import re
|
||||
email_account = frappe.get_doc('Email Account', '_Test Email Account 1')
|
||||
|
|
@ -166,6 +180,6 @@ class TestEmail(unittest.TestCase):
|
|||
self.assertTrue(re.search('''<img[^>]*src=["']/private/files/rtco2.png[^>]*>''', communication.content))
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
if __name__ == '__main__':
|
||||
frappe.connect()
|
||||
unittest.main()
|
||||
|
|
|
|||
25
frappe/tests/test_formatter.py
Normal file
25
frappe/tests/test_formatter.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import frappe
|
||||
from frappe import format
|
||||
import unittest
|
||||
|
||||
class TestFormatter(unittest.TestCase):
|
||||
def test_currency_formatting(self):
|
||||
df = frappe._dict({
|
||||
'fieldname': 'amount',
|
||||
'fieldtype': 'Currency',
|
||||
'options': 'currency'
|
||||
})
|
||||
|
||||
doc = frappe._dict({
|
||||
'amount': 5
|
||||
})
|
||||
frappe.db.set_default("currency", 'INR')
|
||||
|
||||
# if currency field is not passed then default currency should be used.
|
||||
self.assertEqual(format(100, df, doc), '₹ 100.00')
|
||||
|
||||
doc.currency = 'USD'
|
||||
self.assertEqual(format(100, df, doc), "$ 100.00")
|
||||
|
||||
frappe.db.set_default("currency", None)
|
||||
|
|
@ -55,12 +55,15 @@ def format_value(value, df=None, doc=None, currency=None, translated=False):
|
|||
# this is required to show 0 as blank in table columns
|
||||
return ""
|
||||
|
||||
elif df.get("fieldtype") == "Currency" or (df.get("fieldtype")=="Float" and (df.options or "").strip()):
|
||||
return fmt_money(value, precision=get_field_precision(df, doc),
|
||||
currency=currency if currency else (get_field_currency(df, doc) if doc else None))
|
||||
elif df.get("fieldtype") == "Currency":
|
||||
default_currency = frappe.db.get_default("currency")
|
||||
currency = currency or get_field_currency(df, doc) or default_currency
|
||||
return fmt_money(value, precision=get_field_precision(df, doc), currency=currency)
|
||||
|
||||
elif df.get("fieldtype") == "Float":
|
||||
precision = get_field_precision(df, doc)
|
||||
# I don't know why we support currency option for float
|
||||
currency = currency or get_field_currency(df, doc)
|
||||
|
||||
# show 1.000000 as 1
|
||||
# options should not specified
|
||||
|
|
@ -69,7 +72,7 @@ def format_value(value, df=None, doc=None, currency=None, translated=False):
|
|||
if len(temp)==1 or cint(temp[1])==0:
|
||||
precision = 0
|
||||
|
||||
return fmt_money(value, precision=precision)
|
||||
return fmt_money(value, precision=precision, currency=currency)
|
||||
|
||||
elif df.get("fieldtype") == "Percent":
|
||||
return "{}%".format(flt(value, 2))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue