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

This commit is contained in:
Rucha Mahabal 2019-12-23 14:58:36 +05:30
commit ccb99d5e51
71 changed files with 799 additions and 296 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

@ -2,8 +2,13 @@ context('Form', () => {
before(() => {
cy.login();
cy.visit('/desk');
cy.window().its('frappe').then(frappe => {
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();
@ -13,4 +18,22 @@ context('Form', () => {
cy.location('hash').should('eq', '#List/ToDo/List');
cy.get('.list-row').should('contain', 'this is a test todo');
});
it('navigates between documents with child table list filters applied', () => {
cy.visit('/desk#List/Contact');
cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
cy.get('.fieldname-select-area').should('exist');
cy.get('.fieldname-select-area input').type('Number{enter}', { force: true });
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({ force: true });
cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible');
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');
cy.get('.clear-filters.btn').click();
});
});

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

@ -13,6 +13,8 @@ from six import text_type
@click.argument('site')
@click.option('--db-name', help='Database name')
@click.option('--db-type', default='mariadb', type=click.Choice(['mariadb', 'postgres']), help='Optional "postgres" or "mariadb". Default is "mariadb"')
@click.option('--db-host', help='Database Host')
@click.option('--db-port', type=int, help='Database Port')
@click.option('--mariadb-root-username', default='root', help='Root username for MariaDB')
@click.option('--mariadb-root-password', help='Root password for MariaDB')
@click.option('--admin-password', help='Administrator password for new site', default=None)
@ -21,22 +23,22 @@ from six import text_type
@click.option('--source_sql', help='Initiate database with a SQL file')
@click.option('--install-app', multiple=True, help='Install app after installation')
def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin_password=None,
verbose=False, install_apps=None, source_sql=None, force=None, install_app=None,
db_name=None, db_type=None):
verbose=False, install_apps=None, source_sql=None, force=None, install_app=None,
db_name=None, db_type=None, db_host=None, db_port=None):
"Create a new site"
frappe.init(site=site, new_site=True)
_new_site(db_name, site, mariadb_root_username=mariadb_root_username,
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
db_type=db_type)
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
db_type=db_type, db_host=db_host, db_port=db_port)
if len(frappe.utils.get_sites()) == 1:
use(site)
def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=None,
admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False,
reinstall=False, db_type=None):
admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False,
reinstall=False, db_type=None, db_host=None, db_port=None):
"""Install a new Frappe site"""
if not force and os.path.exists(site):
@ -65,8 +67,8 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
installing = touch_file(get_site_path('locks', 'installing.lock'))
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password,
db_name=db_name, admin_password=admin_password, verbose=verbose,
source_sql=source_sql, force=force, reinstall=reinstall, db_type=db_type)
db_name=db_name, admin_password=admin_password, verbose=verbose,
source_sql=source_sql, force=force, reinstall=reinstall, db_type=db_type, db_host=db_host, db_port=db_port)
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
for app in apps_to_install:

View file

@ -398,7 +398,7 @@ def get_bcc(doc, recipients=None, fetched_from_email_account=False):
return bcc
def add_attachments(name, attachments):
'''Add attachments to the given Communiction'''
'''Add attachments to the given Communication'''
# loop through attachments
for a in attachments:
if isinstance(a, string_types):
@ -411,7 +411,9 @@ def add_attachments(name, attachments):
"file_url": attach.file_url,
"attached_to_doctype": "Communication",
"attached_to_name": name,
"folder": "Home/Attachments"})
"folder": "Home/Attachments",
"is_private": attach.is_private
})
_file.save(ignore_permissions=True)
def filter_email_list(doc, email_list, exclude, is_cc=False, is_bcc=False):

View file

@ -89,8 +89,9 @@ class File(Document):
def validate(self):
if self.is_new():
self.set_is_private()
self.set_file_name()
self.validate_duplicate_entry()
self.validate_file_name()
self.validate_folder()
if not self.file_url and not self.flags.ignore_file_validate:
@ -133,6 +134,9 @@ class File(Document):
frappe.db.set_value(self.attached_to_doctype, self.attached_to_name,
self.attached_to_field, self.file_url)
if self.file_url and (self.is_private != self.file_url.startswith('/private')):
frappe.throw(_('Invalid file URL. Please contact System Administrator.'))
def set_folder_name(self):
"""Make parent folders if not exists based on reference doctype and name"""
if self.attached_to_doctype and not self.folder:
@ -157,9 +161,11 @@ class File(Document):
def validate_duplicate_entry(self):
if not self.flags.ignore_duplicate_entry_error and not self.is_folder:
# check duplicate name
if not self.content_hash:
self.generate_content_hash()
# check duplicate assignement
# check duplicate name
# check duplicate assignment
filters = {
'content_hash': self.content_hash,
'is_private': self.is_private,
@ -184,21 +190,20 @@ class File(Document):
else:
self.file_url = duplicate_file.file_url
def validate_file_name(self):
def set_file_name(self):
if not self.file_name and self.file_url:
self.file_name = self.file_url.split('/')[-1]
def generate_content_hash(self):
if self.content_hash or not self.file_url:
if self.content_hash or not self.file_url or self.file_url.startswith('http'):
return
if self.file_url.startswith("/files/"):
try:
with open(get_files_path(self.file_name.lstrip("/")), "rb") as f:
self.content_hash = get_content_hash(f.read())
except IOError:
frappe.msgprint(_("File {0} does not exist").format(self.file_url))
raise
try:
with open(get_files_path(self.file_name.lstrip("/"), is_private=self.is_private), "rb") as f:
self.content_hash = get_content_hash(f.read())
except IOError:
frappe.msgprint(_("File {0} does not exist").format(self.file_url))
raise
def on_trash(self):
if self.is_home_folder or self.is_attachments_folder:
@ -563,6 +568,9 @@ class File(Document):
except frappe.DoesNotExistError:
frappe.clear_messages()
def set_is_private(self):
if self.file_url:
self.is_private = cint(self.file_url.startswith('/private'))
def on_doctype_update():
frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"])

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

@ -80,12 +80,14 @@ class DbManager:
if pipe:
print('Creating Database...')
command = '{pipe} mysql -u {user} -p{password} -h{host} {target} {source}'.format(
command = '{pipe} mysql -u {user} -p{password} -h{host} ' + ('-P{port}' if frappe.db.port else '') + ' {target} {source}'
command = command.format(
pipe=pipe,
user=esc(user),
password=esc(password),
host=esc(frappe.db.host),
target=esc(target),
source=source
source=source,
port=frappe.db.port
)
os.system(command)

View file

@ -61,7 +61,7 @@ def update_global_search_doctypes():
if search_doctypes.get(domain):
global_search_doctypes.extend(search_doctypes.get(domain))
doctype_list = set([dt.name for dt in frappe.get_list("DocType")])
doctype_list = set([dt.name for dt in frappe.get_all("DocType")])
allowed_in_global_search = []
for dt in global_search_doctypes:

View file

@ -105,7 +105,7 @@ def get_next(doctype, value, prev, filters, sort_order, sort_field):
res = frappe.get_list(doctype,
fields = ["name"],
filters = filters,
order_by = sort_field + " " + sort_order,
order_by = "`tab{0}`.{1}".format(doctype, sort_field) + " " + sort_order,
limit_start=0, limit_page_length=1, as_list=True)
if not res:

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

@ -19,6 +19,14 @@
background: #f0f4f7;
}
.from-date-field .clearfix{
display: none;
}
.from-date-field {
margin-left: 10px;
}
.select-time:focus, .select-doctype:focus, .select-filter:focus, .select-sort:focus {
background: #f0f4f7;
}

View file

@ -41,7 +41,11 @@ class Leaderboard {
return field;
});
}
this.timespans = ["Week", "Month", "Quarter", "Year", "All Time"];
this.timespans = [
"This Week", "This Month", "This Quarter", "This Year",
"Last Week", "Last Month", "Last Quarter", "Last Year",
"All Time", "Select From Date"
];
// for saving current selected filters
const _initial_doctype = frappe.get_route()[1] || this.doctypes[0];
@ -103,7 +107,8 @@ class Leaderboard {
this.timespans.map(d => {
return {"label": __(d), value: d };
})
);
);
this.create_from_date_field();
this.type_select = this.page.add_select(__("Field"),
this.options.selected_filter.map(d => {
@ -113,7 +118,12 @@ class Leaderboard {
this.timespan_select.on("change", (e) => {
this.options.selected_timespan = e.currentTarget.value;
this.make_request();
if (this.options.selected_timespan === 'Select From Date') {
this.from_date_field.show();
} else {
this.from_date_field.hide();
this.make_request();
}
});
this.type_select.on("change", (e) => {
@ -122,6 +132,28 @@ class Leaderboard {
});
}
create_from_date_field() {
let timespan_field = $(this.parent).find(`.frappe-control[data-original-title='Timespan']`);
this.from_date_field = $(`<div class="from-date-field"></div>`).insertAfter(timespan_field).hide();
let date_field = frappe.ui.form.make_control({
df: {
fieldtype: 'Date',
fieldname: 'selected_from_date',
placeholder: frappe.datetime.month_start(),
default: frappe.datetime.month_start(),
input_class: 'input-sm',
reqd: 1,
change: () => {
this.selected_from_date = date_field.get_value();
if (this.selected_from_date) this.make_request();
}
},
parent: $(this.parent).find('.from-date-field'),
render_input: 1
});
}
render_selected_doctype() {
this.$sidebar_list.on("click", "li", (e)=> {
@ -207,7 +239,6 @@ class Leaderboard {
this.leaderboard_config[this.options.selected_doctype].method,
{
'from_date': this.get_from_date(),
'timespan': this.options.selected_timespan,
'company': this.options.selected_company,
'field': this.options.selected_filter_item,
'limit': this.leaderboard_limit,
@ -360,17 +391,20 @@ class Leaderboard {
get_from_date() {
let timespan = this.options.selected_timespan.toLowerCase();
let current_date = frappe.datetime.now_date();
let date = '';
if (timespan === "month") {
date = frappe.datetime.add_months(current_date, -1);
} else if (timespan === "quarter") {
date = frappe.datetime.add_months(current_date, -3);
} else if (timespan === "year") {
date = frappe.datetime.add_months(current_date, -12);
} else if (timespan === "week") {
date = frappe.datetime.add_days(current_date, -7);
let get_from_date = {
"this week": frappe.datetime.week_start(),
"this month": frappe.datetime.month_start(),
"this quarter": frappe.datetime.quarter_start(),
"this year": frappe.datetime.year_start(),
"last week": frappe.datetime.add_days(current_date, -7),
"last month": frappe.datetime.add_months(current_date, -1),
"last quarter": frappe.datetime.add_months(current_date, -3),
"last year": frappe.datetime.add_months(current_date, -12),
"all time": "",
"select from date": this.selected_from_date || frappe.datetime.month_start()
}
return date;
return get_from_date[timespan];
}
}

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

@ -50,7 +50,7 @@ def sanitize_searchfield(searchfield):
# this is called by the Link Field
@frappe.whitelist()
def search_link(doctype, txt, query=None, filters=None, page_length=20, searchfield=None, reference_doctype=None, ignore_user_permissions=False):
search_widget(doctype, txt, query, searchfield=searchfield, page_length=page_length, filters=filters, reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions)
search_widget(doctype, txt.strip(), query, searchfield=searchfield, page_length=page_length, filters=filters, reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions)
frappe.response['results'] = build_for_autosuggest(frappe.response["values"])
del frappe.response["values"]

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

@ -456,9 +456,9 @@ class Email:
def show_attached_email_headers_in_content(self, part):
# get the multipart/alternative message
try:
from html import escape # python 3.x
from html import escape # python 3.x
except ImportError:
from cgi import escape # python 2.x
from cgi import escape # python 2.x
message = list(part.walk())[1]
headers = []
@ -480,7 +480,7 @@ class Email:
"""Detect chartset."""
charset = part.get_content_charset()
if not charset:
charset = chardet.detect(frappe.safe_encode(part))['encoding']
charset = chardet.detect(cstr(part))['encoding']
return charset
@ -514,7 +514,7 @@ class Email:
'fcontent': fcontent,
})
cid = (part.get("Content-Id") or "").strip("><")
cid = (cstr(part.get("Content-Id")) or "").strip("><")
if cid:
self.cid_map[fname] = cid

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

@ -7,10 +7,5 @@ frappe.ui.form.on('Currency', {
if(!frm.doc.enabled) {
frm.set_intro(__("This Currency is disabled. Enable to use in transactions"));
}
},
after_save(frm) {
if (frm.doc.enabled)
locals[':Currency'][frm.doc.name] = Object.assign(frm.doc, { doctype: ':Currency' });
}
});

View file

@ -21,13 +21,13 @@ from frappe.database import setup_database
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
def install_db(root_login="root", root_password=None, db_name=None, source_sql=None,
admin_password=None, verbose=True, force=0, site_config=None, reinstall=False,
db_type=None):
admin_password=None, verbose=True, force=0, site_config=None, reinstall=False,
db_type=None, db_host=None, db_port=None):
if not db_type:
db_type = frappe.conf.db_type or 'mariadb'
make_conf(db_name, site_config=site_config, db_type=db_type)
make_conf(db_name, site_config=site_config, db_type=db_type, db_host=db_host, db_port=db_port)
frappe.flags.in_install_db = True
frappe.flags.root_login = root_login
@ -191,14 +191,14 @@ def init_singles():
doc.flags.ignore_validate=True
doc.save()
def make_conf(db_name=None, db_password=None, site_config=None, db_type=None):
def make_conf(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None):
site = frappe.local.site
make_site_config(db_name, db_password, site_config, db_type=db_type)
make_site_config(db_name, db_password, site_config, db_type=db_type, db_host=db_host, db_port=db_port)
sites_path = frappe.local.sites_path
frappe.destroy()
frappe.init(site, sites_path=sites_path)
def make_site_config(db_name=None, db_password=None, site_config=None, db_type=None):
def make_site_config(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None):
frappe.create_folder(os.path.join(frappe.local.site_path))
site_file = get_site_config_path()
@ -209,6 +209,12 @@ def make_site_config(db_name=None, db_password=None, site_config=None, db_type=N
if db_type:
site_config['db_type'] = db_type
if db_host:
site_config['db_host'] = db_host
if db_port:
site_config['db_port'] = db_port
with open(site_file, "w") as f:
f.write(json.dumps(site_config, indent=1, sort_keys=True))

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

@ -501,6 +501,10 @@ class DatabaseQuery(object):
value = f.value or "''"
fallback = "''"
elif f.fieldname == 'name':
value = f.value or "''"
fallback = "''"
else:
value = flt(f.value)
fallback = 0

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

@ -3,6 +3,9 @@ import frappe
def execute():
if frappe.db.count("File", filters={"attached_to_doctype": "Prepared Report", "is_private": 0}) > 10000:
frappe.db.auto_commit_on_many_writes = True
files = frappe.get_all("File", fields=["name", "attached_to_name"], filters={"attached_to_doctype": "Prepared Report", "is_private": 0})
for file_dict in files:
# For some reason Prepared Report doc might not exist, check if it exists first
@ -17,3 +20,7 @@ def execute():
else:
# If Prepared Report doc doesn't exist then the file doc is useless. Delete it.
frappe.delete_doc("File", file_dict.name)
if frappe.db.auto_commit_on_many_writes:
frappe.db.auto_commit_on_many_writes = False

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

@ -10,13 +10,7 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({
set_options() {
if (this.df.options) {
let options = this.df.options || [];
if (typeof options === 'string') {
options = options.split('\n');
}
if (typeof options[0] === 'string') {
options = options.map(o => ({ label: o, value: o }));
}
this._data = options;
this._data = this.parse_options(options);
}
},
@ -100,6 +94,9 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({
},
validate(value) {
if (this.df.ignore_validation) {
return value || '';
}
let valid_values = this.awesomplete._list.map(d => d.value);
if (!valid_values.length) {
return value;
@ -111,11 +108,22 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({
}
},
parse_options(options) {
if (typeof options === 'string') {
options = options.split('\n');
}
if (typeof options[0] === 'string') {
options = options.map(o => ({ label: o, value: o }));
}
return options;
},
get_data() {
return this._data || [];
},
set_data(data) {
data = this.parse_options(data);
if (this.awesomplete) {
this.awesomplete.list = data;
}

View file

@ -4,11 +4,6 @@ frappe.ui.form.ControlCurrency = frappe.ui.form.ControlFloat.extend({
return isNaN(parseFloat(value)) ? "" : formatted_value;
},
get_number_format: function() {
var currency = frappe.meta.get_field_currency(this.df, this.get_doc());
return get_number_format(currency);
},
get_precision: function() {
// always round based on field precision or currency's precision
// this method is also called in this.parse()

View file

@ -1,10 +1,7 @@
frappe.ui.form.ControlFloat = frappe.ui.form.ControlInt.extend({
parse: function(value) {
value = this.eval_expression(value);
return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision(),
// While parsing currency, get_number_format passes currency's number_format
// In case of parsing float, it passes global number_format
this.get_number_format());
return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision());
},
format_for_input: function(value) {
@ -17,8 +14,8 @@ frappe.ui.form.ControlFloat = frappe.ui.form.ControlInt.extend({
},
get_number_format: function() {
// In case of 'Float' field currency's number_format shouldn't be used for formatting
return get_number_format();
var currency = frappe.meta.get_field_currency(this.df, this.get_doc());
return get_number_format(currency);
},
get_precision: function() {

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

@ -81,6 +81,7 @@ frappe.ui.form.Review = class Review {
label: __('To User'),
reqd: 1,
options: user_options,
ignore_validation: 1,
description: __('Only users involved in the document are listed')
}, {
fieldname: 'review_type',

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() {
@ -614,7 +614,7 @@ class FilterArea {
let options = df.options;
let condition = '=';
let fieldtype = df.fieldtype;
if (['Text', 'Small Text', 'Text Editor', 'Data', 'Code'].includes(fieldtype)) {
if (['Text', 'Small Text', 'Text Editor', 'HTML Editor', 'Data', 'Code'].includes(fieldtype)) {
fieldtype = 'Data';
condition = 'like';
}
@ -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

@ -90,6 +90,14 @@ $.extend(frappe.datetime, {
return moment().endOf("month").format();
},
quarter_start: function() {
return moment().startOf("quarter").format();
},
quarter_end: function() {
return moment().endOf("quarter").format();
},
year_start: function(){
return moment().startOf("year").format();
},

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)
@ -144,10 +144,7 @@ function get_currency_symbol(currency) {
}
function get_number_format(currency) {
let format = null;
if (currency) format = frappe.model.get_value(":Currency", currency, "number_format");
return format || (frappe.boot && frappe.boot.sysdefaults && frappe.boot.sysdefaults.number_format) || "#,###.##";
return (frappe.boot && frappe.boot.sysdefaults && frappe.boot.sysdefaults.number_format) || "#,###.##";
}
function get_number_format_info(format) {

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

@ -239,6 +239,18 @@
.progress-area {
padding-top: 15px;
padding-bottom: 15px;
.progress-chart {
padding-top: 15px;
}
.progress {
margin-bottom: 5px;
}
.progress-message {
margin-top: 0px;
}
}
.form-links {

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

@ -10,7 +10,8 @@
// To compensate for percieved centering
.null-state {
height: 12em !important;
height: 15rem !important;
max-height: 150px;
width: auto;
}
@ -49,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;
@ -292,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;
@ -303,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;
@ -616,4 +636,4 @@ input.list-check-all, input.list-row-checkbox {
.file-title {
margin-top: 5px;
}
}

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

@ -73,4 +73,22 @@ def create_contact_phone_nos_records():
doc.first_name = 'Test Contact'
for index in range(1000):
doc.append('phone_nos', {'phone': '123456{}'.format(index)})
doc.insert()
doc.insert()
@frappe.whitelist()
def create_contact_records():
if frappe.db.get_all('Contact', {'first_name': 'Test Form Contact 1'}):
return
insert_contact('Test Form Contact 1', '12345')
insert_contact('Test Form Contact 2', '54321')
insert_contact('Test Form Contact 3', '12345')
def insert_contact(first_name, phone_number):
doc = frappe.get_doc({
'doctype': 'Contact',
'first_name': first_name
})
doc.append('phone_nos', {'phone': phone_number})
doc.insert()

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

View file

@ -14,6 +14,7 @@ import frappe, os, time
import schedule
from frappe.utils import now_datetime, get_datetime
from frappe.utils import get_sites
from frappe.installer import update_site_config
from frappe.core.doctype.user.user import STANDARD_USERS
from frappe.utils.background_jobs import get_jobs