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