Merge branch 'develop' into mandatory-depends-on

This commit is contained in:
Suraj Shetty 2019-12-23 13:03:49 +05:30 committed by GitHub
commit 480a192324
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 593 additions and 219 deletions

View 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
};

View file

@ -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');

View file

@ -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);
});
});

View 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);
});
});
});
});

View file

@ -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;
});
});
});

View file

@ -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()

View file

@ -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))]

View file

@ -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):

View file

@ -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",

View file

@ -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):

View file

@ -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'''

View file

@ -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)

View file

@ -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):

View file

@ -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}}

View file

@ -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)

View file

@ -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.

View file

@ -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)

View file

@ -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",

View file

@ -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:

View file

@ -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")

View 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')

View 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()

View file

@ -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",

View file

@ -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>
{% } %}

View file

@ -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"
});

View file

@ -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>

View file

@ -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);

View file

@ -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'});
}
});

View file

@ -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);

View file

@ -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 () {

View file

@ -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());
});
}
});
}
}

View file

@ -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,
};
}));

View file

@ -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 {

View file

@ -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() {

View file

@ -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)) {

View file

@ -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>");

View file

@ -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)

View file

@ -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

View file

@ -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) {

View file

@ -157,6 +157,11 @@ a.badge-hover& {
}
}
.msgprint-scroll {
max-height: 36em;
overflow: scroll;
}
.msgprint {
// margin: 15px 0px;
// text-align: center;

View file

@ -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;
}
}

View file

@ -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;

View file

@ -76,7 +76,6 @@
}
.modal-header .indicator {
float: left;
margin-top: 7.5px;
margin-right: 3px;
}

View file

@ -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;

View file

@ -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;

View file

@ -406,6 +406,10 @@ body[data-route^="Module"] .main-menu {
.dropdown-search {
padding: 8px;
}
.stat-no-records {
margin: 5px 10px;
}
}
// module sidebar

View file

@ -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()

View 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)

View file

@ -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))