Merge branch 'develop' of https://github.com/frappe/frappe into feat-desktop-cards-reorder
This commit is contained in:
commit
4cbd74a48d
482 changed files with 234088 additions and 231176 deletions
|
|
@ -58,8 +58,6 @@
|
|||
"frappe": true,
|
||||
"Vue": true,
|
||||
"__": true,
|
||||
"_p": true,
|
||||
"_f": true,
|
||||
"repl": true,
|
||||
"Class": true,
|
||||
"locals": true,
|
||||
|
|
|
|||
13
.mergify.yml
Normal file
13
.mergify.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
pull_request_rules:
|
||||
- name: Automatic merge on CI success and review
|
||||
conditions:
|
||||
- status-success=Codacy/PR Quality Review
|
||||
- status-success=Semantic Pull Request
|
||||
- status-success=continuous-integration/travis-ci/pr
|
||||
- status-success=security/snyk - package.json (frappe)
|
||||
- status-success=security/snyk - requirements.txt (frappe)
|
||||
- label!=don't-merge
|
||||
- "#approved-reviews-by>=1"
|
||||
actions:
|
||||
merge:
|
||||
method: merge
|
||||
14
.snyk
14
.snyk
|
|
@ -2,8 +2,16 @@
|
|||
version: v1.13.3
|
||||
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
|
||||
ignore:
|
||||
SNYK-JS-AWESOMPLETE-174474:
|
||||
- awesomplete:
|
||||
reason: No patch available
|
||||
expires: '2019-06-11T14:12:04.995Z'
|
||||
'npm:mem:20180117':
|
||||
- showdown > yargs > os-locale > mem:
|
||||
reason: None given
|
||||
expires: '2019-04-01T10:08:52.588Z'
|
||||
patch: {}
|
||||
reason: No patch available
|
||||
expires: '2019-06-11T14:12:04.995Z'
|
||||
# patches apply the minimum changes required to fix a vulnerability
|
||||
patch:
|
||||
'npm:extend:20180424':
|
||||
- superagent > extend:
|
||||
patched: '2019-05-09T10:14:19.246Z'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"baseUrl": "http://test_site_ui:8000",
|
||||
"projectId": "92odwv"
|
||||
"projectId": "92odwv",
|
||||
"adminPassword": "admin"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
context('Awesome Bar', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login('Administrator', 'qwe');
|
||||
cy.login();
|
||||
cy.visit('/desk');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.get('.navbar-home').click();
|
||||
cy.get('.navbar-header .navbar-home').click();
|
||||
});
|
||||
|
||||
it('navigates to doctype list', () => {
|
||||
cy.get('#navbar-search')
|
||||
.type('todo{downarrow}{enter}', { delay: 100 });
|
||||
cy.get('#navbar-search').type('todo', { delay: 200 });
|
||||
cy.get('#navbar-search + ul').should('be.visible');
|
||||
cy.get('#navbar-search').type('{downarrow}{enter}', { delay: 100 });
|
||||
|
||||
cy.get('h1').should('contain', 'To Do');
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ context('Awesome Bar', () => {
|
|||
|
||||
it('find text in doctype list', () => {
|
||||
cy.get('#navbar-search')
|
||||
.type('test in todo{downarrow}{enter}', { delay: 100 });
|
||||
.type('test in todo{downarrow}{enter}', { delay: 200 });
|
||||
|
||||
cy.get('h1').should('contain', 'To Do');
|
||||
|
||||
|
|
@ -31,14 +32,14 @@ context('Awesome Bar', () => {
|
|||
|
||||
it('navigates to new form', () => {
|
||||
cy.get('#navbar-search')
|
||||
.type('new blog post{downarrow}{enter}', { delay: 100 });
|
||||
.type('new blog post{downarrow}{enter}', { delay: 200 });
|
||||
|
||||
cy.get('.title-text:visible').should('have.text', 'New Blog Post 1');
|
||||
});
|
||||
|
||||
it('calculates math expressions', () => {
|
||||
cy.get('#navbar-search')
|
||||
.type('55 + 32{downarrow}{enter}', { delay: 100 });
|
||||
.type('55 + 32{downarrow}{enter}', { delay: 200 });
|
||||
|
||||
cy.get('.modal-title').should('contain', 'Result');
|
||||
cy.get('.msgprint').should('contain', '55 + 32 = 87');
|
||||
|
|
|
|||
75
cypress/integration/control_link.js
Normal file
75
cypress/integration/control_link.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
context('Control Link', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk');
|
||||
cy.create_records({
|
||||
doctype: 'ToDo',
|
||||
description: 'this is a test todo for link'
|
||||
}).as('todos');
|
||||
});
|
||||
|
||||
function get_dialog_with_link() {
|
||||
return cy.dialog({
|
||||
title: 'Link',
|
||||
fields: [
|
||||
{
|
||||
'label': 'Select ToDo',
|
||||
'fieldname': 'link',
|
||||
'fieldtype': 'Link',
|
||||
'options': 'ToDo'
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
it('should set the valid value', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=link] input')
|
||||
.focus()
|
||||
.type('todo for li')
|
||||
.type('n', { delay: 600 })
|
||||
.type('k', { delay: 700 });
|
||||
cy.wait('@search_link');
|
||||
cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname=link] input').type('{downarrow}{enter}', { delay: 100 });
|
||||
cy.get('.frappe-control[data-fieldname=link] input').blur();
|
||||
cy.get('@dialog').then(dialog => {
|
||||
cy.get('@todos').then(todos => {
|
||||
let value = dialog.get_value('link');
|
||||
expect(value).to.eq(todos[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.only('should unset invalid value', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
cy.server();
|
||||
cy.route('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=link] input')
|
||||
.type('invalid value', { delay: 100 })
|
||||
.blur();
|
||||
cy.wait('@validate_link');
|
||||
cy.get('.frappe-control[data-fieldname=link] input').should('have.value', '');
|
||||
});
|
||||
|
||||
it('should route to form on arrow click', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
cy.server();
|
||||
cy.route('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
|
||||
|
||||
cy.get('@todos').then(todos => {
|
||||
cy.get('.frappe-control[data-fieldname=link] input').type(todos[0]).blur();
|
||||
cy.wait('@validate_link');
|
||||
cy.get('.frappe-control[data-fieldname=link] input').focus();
|
||||
cy.get('.frappe-control[data-fieldname=link] .link-btn').click();
|
||||
cy.location('hash').should('eq', `#Form/ToDo/${todos[0]}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,14 +1,21 @@
|
|||
context('Rating Control', () => {
|
||||
beforeEach(() => {
|
||||
cy.login('Administrator', 'qwe');
|
||||
context('Control Rating', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk');
|
||||
});
|
||||
|
||||
function get_dialog_with_rating() {
|
||||
return cy.dialog({
|
||||
title: 'Rating',
|
||||
fields: [{
|
||||
'fieldname': 'rate',
|
||||
'fieldtype': 'Rating',
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
it('click on the star rating to record value', () => {
|
||||
cy.visit('/desk');
|
||||
cy.dialog('Rating', [{
|
||||
'fieldname': 'rate',
|
||||
'fieldtype': 'Rating',
|
||||
}]).as('dialog');
|
||||
get_dialog_with_rating().as('dialog');
|
||||
|
||||
cy.get('div.rating')
|
||||
.children('i.fa')
|
||||
|
|
@ -18,15 +25,13 @@ context('Rating Control', () => {
|
|||
cy.get('@dialog').then(dialog => {
|
||||
var value = dialog.get_value('rate');
|
||||
expect(value).to.equal(1);
|
||||
dialog.hide();
|
||||
});
|
||||
});
|
||||
|
||||
it('hover on the star', () => {
|
||||
cy.visit('/desk');
|
||||
cy.dialog('Rating', [{
|
||||
'fieldname': 'rate',
|
||||
'fieldtype': 'Rating',
|
||||
}]);
|
||||
get_dialog_with_rating();
|
||||
|
||||
cy.get('div.rating')
|
||||
.children('i.fa')
|
||||
.first()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
context('FileUploader', () => {
|
||||
before(() => {
|
||||
cy.login('Administrator', 'qwe');
|
||||
cy.login();
|
||||
cy.visit('/desk');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
context('Form', () => {
|
||||
before(() => {
|
||||
cy.login('Administrator', 'qwe');
|
||||
cy.login();
|
||||
cy.visit('/desk');
|
||||
});
|
||||
|
||||
|
|
|
|||
33
cypress/integration/list_view.js
Normal file
33
cypress/integration/list_view.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
context('List View', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk');
|
||||
cy.window().its('frappe').then(frappe => {
|
||||
frappe.call("frappe.tests.ui_test_helpers.setup_workflow");
|
||||
});
|
||||
cy.clear_cache();
|
||||
});
|
||||
it('enables "Actions" button', () => {
|
||||
const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Print','Delete'];
|
||||
cy.go_to_list('ToDo');
|
||||
cy.get('.level-item.list-row-checkbox.hidden-xs').click({ multiple: true, force: true });
|
||||
cy.get('.btn.btn-primary.btn-sm.dropdown-toggle').contains('Actions').should('be.visible').click();
|
||||
cy.get('.dropdown-menu li:visible').should('have.length', 6).each((el, index) => {
|
||||
cy.wrap(el).contains(actions[index]);
|
||||
}).then((elements) => {
|
||||
cy.server();
|
||||
cy.route({
|
||||
method: 'POST',
|
||||
url:'api/method/frappe.model.workflow.bulk_workflow_approval'
|
||||
}).as('bulk-approval');
|
||||
cy.route({
|
||||
method: 'GET',
|
||||
url:'api/method/frappe.desk.reportview.get*'
|
||||
}).as('update-list');
|
||||
cy.wrap(elements).contains('Approve').click();
|
||||
cy.wait(['@bulk-approval', '@update-list']);
|
||||
cy.get('.list-row-container:visible').should('contain', 'Approved');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
context('List View Settings', () => {
|
||||
beforeEach(() => {
|
||||
cy.login('Administrator', 'qwe');
|
||||
cy.login();
|
||||
cy.visit('/desk');
|
||||
});
|
||||
it('Default settings', () => {
|
||||
cy.visit('/desk#List/DocType/List');
|
||||
cy.get('.list-count').should('contain', "20 of");
|
||||
cy.get('.sidebar-stat').should('contain', "No Tags");
|
||||
cy.get('.sidebar-stat').should('contain', "Tags");
|
||||
});
|
||||
it('disable count and sidebar stats then verify', () => {
|
||||
cy.visit('/desk#List/DocType/List');
|
||||
|
|
@ -14,13 +14,13 @@ context('List View Settings', () => {
|
|||
cy.get('button').contains('Menu').click();
|
||||
cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click();
|
||||
cy.get('.modal-dialog').should('contain', 'Settings');
|
||||
|
||||
|
||||
cy.get('input[data-fieldname="disable_count"]').check({force: true});
|
||||
cy.get('input[data-fieldname="disable_sidebar_stats"]').check({force: true});
|
||||
cy.get('button').filter(':visible').contains('Save').click();
|
||||
|
||||
|
||||
cy.reload();
|
||||
|
||||
|
||||
cy.get('.list-count').should('be.empty');
|
||||
cy.get('.list-sidebar .sidebar-stat').should('not.exist');
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ context('Login', () => {
|
|||
|
||||
it('logs in using correct credentials', () => {
|
||||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('#login_password').type('qwe');
|
||||
cy.get('#login_password').type(Cypress.config('adminPassword'));
|
||||
|
||||
cy.get('.btn-login').click();
|
||||
cy.location('pathname').should('eq', '/desk');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
context('Form', () => {
|
||||
before(() => {
|
||||
cy.login('Administrator', 'qwe');
|
||||
cy.login();
|
||||
cy.visit('/desk');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
context('Recorder', () => {
|
||||
before(() => {
|
||||
cy.login('Administrator', 'qwe');
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('Navigate to Recorder', () => {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
context('Relative Timeframe', () => {
|
||||
beforeEach(() => {
|
||||
cy.login('Administrator', 'qwe');
|
||||
cy.login();
|
||||
cy.visit('/desk');
|
||||
});
|
||||
before(() => {
|
||||
cy.login('Administrator', 'qwe');
|
||||
cy.login();
|
||||
cy.visit('/desk');
|
||||
cy.window().its('frappe').then(frappe => {
|
||||
frappe.call("frappe.tests.test_utils.create_todo_records");
|
||||
frappe.call("frappe.tests.ui_test_helpers.create_todo_records");
|
||||
});
|
||||
});
|
||||
it('set relative filter for Previous and check list', () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
context('Table MultiSelect', () => {
|
||||
beforeEach(() => {
|
||||
cy.login('Administrator', 'qwe');
|
||||
cy.login();
|
||||
});
|
||||
|
||||
let name = 'table multiselect' + Math.random().toString().slice(2, 8);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@ import 'cypress-file-upload';
|
|||
// -- This is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... });
|
||||
Cypress.Commands.add('login', (email, password) => {
|
||||
if (!email) {
|
||||
email = 'Administrator';
|
||||
}
|
||||
if (!password) {
|
||||
password = Cypress.config('adminPassword');
|
||||
}
|
||||
cy.request({
|
||||
url: '/api/method/login',
|
||||
method: 'POST',
|
||||
|
|
@ -35,6 +41,29 @@ Cypress.Commands.add('login', (email, password) => {
|
|||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('call', (method, args) => {
|
||||
return cy.window().its('frappe.csrf_token').then(csrf_token => {
|
||||
return cy.request({
|
||||
url: `/api/method/${method}`,
|
||||
method: 'POST',
|
||||
body: args,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Frappe-CSRF-Token': csrf_token
|
||||
}
|
||||
}).then(res => {
|
||||
expect(res.status).eq(200);
|
||||
return res.body;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('create_records', (doc) => {
|
||||
return cy.call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc })
|
||||
.then(r => r.message);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('fill_field', (fieldname, value, fieldtype='Data') => {
|
||||
let selector = `.form-control[data-fieldname="${fieldname}"]`;
|
||||
|
||||
|
|
@ -66,15 +95,15 @@ Cypress.Commands.add('go_to_list', (doctype) => {
|
|||
cy.visit(`/desk#List/${doctype}/List`);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('dialog', (title, fields) => {
|
||||
cy.window().then(win => {
|
||||
var d = new win.frappe.ui.Dialog({
|
||||
title: title,
|
||||
fields: fields,
|
||||
primary_action: function(){
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
Cypress.Commands.add('clear_cache', () => {
|
||||
cy.window().its('frappe').then(frappe => {
|
||||
frappe.ui.toolbar.clear_cache();
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('dialog', (opts) => {
|
||||
return cy.window().then(win => {
|
||||
var d = new win.frappe.ui.Dialog(opts);
|
||||
d.show();
|
||||
return d;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ if sys.version[0] == '2':
|
|||
reload(sys)
|
||||
sys.setdefaultencoding("utf-8")
|
||||
|
||||
__version__ = '11.1.21'
|
||||
__version__ = '11.1.36'
|
||||
__title__ = "Frappe Framework"
|
||||
|
||||
local = Local()
|
||||
|
|
@ -339,6 +339,10 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
|
|||
out.alert = 1
|
||||
|
||||
message_log.append(json.dumps(out))
|
||||
|
||||
if raise_exception and hasattr(raise_exception, '__name__'):
|
||||
local.response['exc_type'] = raise_exception.__name__
|
||||
|
||||
_raise_exception()
|
||||
|
||||
def clear_messages():
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ from frappe import _
|
|||
from six.moves.urllib.parse import urlparse, urlencode
|
||||
import base64
|
||||
|
||||
|
||||
def handle():
|
||||
"""
|
||||
Handler for `/api` methods
|
||||
|
|
@ -181,4 +180,4 @@ def validate_api_key_secret(api_key, api_secret):
|
|||
user_secret = frappe.utils.password.get_decrypted_password ("User", user, fieldname='api_secret')
|
||||
if api_secret == user_secret:
|
||||
frappe.set_user(user)
|
||||
frappe.local.form_dict = form_dict
|
||||
frappe.local.form_dict = form_dict
|
||||
|
|
|
|||
|
|
@ -109,7 +109,8 @@ def init_request(request):
|
|||
raise NotFound
|
||||
|
||||
if frappe.local.conf.get('maintenance_mode'):
|
||||
raise frappe.SessionStopped
|
||||
frappe.connect()
|
||||
raise frappe.SessionStopped('Session Stopped')
|
||||
|
||||
make_form_dict(request)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from frappe import _
|
|||
import frappe
|
||||
import frappe.database
|
||||
import frappe.utils
|
||||
from frappe.utils import cint, flt, get_datetime, datetime
|
||||
from frappe.utils import cint, flt, get_datetime, datetime, date_diff, today
|
||||
import frappe.utils.user
|
||||
from frappe import conf
|
||||
from frappe.sessions import Session, clear_sessions, delete_session
|
||||
|
|
@ -124,6 +124,12 @@ class LoginManager:
|
|||
frappe.clear_cache(user = frappe.form_dict.get('usr'))
|
||||
user, pwd = get_cached_user_pass()
|
||||
self.authenticate(user=user, pwd=pwd)
|
||||
if self.force_user_to_reset_password():
|
||||
doc = frappe.get_doc("User", self.user)
|
||||
frappe.local.response["redirect_to"] = doc.reset_password(send_email=False, password_expired=True)
|
||||
frappe.local.response["message"] = "Password Reset"
|
||||
return False
|
||||
|
||||
if should_run_2fa(self.user):
|
||||
authenticate_for_2factor(self.user)
|
||||
if not confirm_otp_token(self):
|
||||
|
|
@ -209,6 +215,22 @@ class LoginManager:
|
|||
self.check_if_enabled(user)
|
||||
self.user = self.check_password(user, pwd)
|
||||
|
||||
def force_user_to_reset_password(self):
|
||||
if not self.user:
|
||||
return
|
||||
|
||||
reset_pwd_after_days = cint(frappe.db.get_single_value("System Settings",
|
||||
"force_user_to_reset_password"))
|
||||
|
||||
if reset_pwd_after_days:
|
||||
last_password_reset_date = frappe.db.get_value("User",
|
||||
self.user, "last_password_reset_date") or today()
|
||||
|
||||
last_pwd_reset_days = date_diff(today(), last_password_reset_date)
|
||||
|
||||
if last_pwd_reset_days > reset_pwd_after_days:
|
||||
return True
|
||||
|
||||
def check_if_enabled(self, user):
|
||||
"""raise exception if user not enabled"""
|
||||
doc = frappe.get_doc("System Settings")
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ class AssignmentRule(Document):
|
|||
doctype = doc.get('doctype'),
|
||||
name = doc.get('name'),
|
||||
description = frappe.render_template(self.description, doc),
|
||||
assignment_rule = self.name
|
||||
assignment_rule = self.name,
|
||||
notify = True
|
||||
))
|
||||
|
||||
# set for reference in round robin
|
||||
|
|
|
|||
104
frappe/automation/doctype/auto_repeat/auto_repeat.js
Normal file
104
frappe/automation/doctype/auto_repeat/auto_repeat.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright (c) 2018, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
frappe.provide("frappe.auto_repeat");
|
||||
|
||||
frappe.ui.form.on('Auto Repeat', {
|
||||
setup: function(frm) {
|
||||
frm.fields_dict['reference_doctype'].get_query = function() {
|
||||
return {
|
||||
query: "frappe.automation.doctype.auto_repeat.auto_repeat.get_auto_repeat_doctypes"
|
||||
};
|
||||
};
|
||||
|
||||
frm.fields_dict['reference_document'].get_query = function() {
|
||||
return {
|
||||
filters: {
|
||||
"auto_repeat": ''
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
frm.fields_dict['print_format'].get_query = function() {
|
||||
return {
|
||||
filters: {
|
||||
"doc_type": frm.doc.reference_doctype
|
||||
}
|
||||
};
|
||||
};
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
// auto repeat message
|
||||
if (frm.is_new()) {
|
||||
let customize_form_link = `<a href="#Form/Customize Form">${__('Customize Form')}</a>`;
|
||||
frm.dashboard.set_headline(__('To configure Auto Repeat, enable "Allow Auto Repeat" from {0}.', [customize_form_link]));
|
||||
}
|
||||
|
||||
// view document button
|
||||
if (!frm.is_dirty()) {
|
||||
let label = __('View {0}', [__(frm.doc.reference_doctype)]);
|
||||
frm.add_custom_button(label, () =>
|
||||
frappe.set_route("List", frm.doc.reference_doctype, { auto_repeat: frm.doc.name })
|
||||
);
|
||||
}
|
||||
|
||||
// auto repeat schedule
|
||||
frappe.auto_repeat.render_schedule(frm);
|
||||
},
|
||||
|
||||
template: function(frm) {
|
||||
if (frm.doc.template) {
|
||||
frappe.model.with_doc("Email Template", frm.doc.template, () => {
|
||||
let email_template = frappe.get_doc("Email Template", frm.doc.template);
|
||||
frm.set_value("subject", email_template.subject);
|
||||
frm.set_value("message", email_template.response);
|
||||
frm.refresh_field("subject");
|
||||
frm.refresh_field("message");
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
get_contacts: function(frm) {
|
||||
frm.call('fetch_linked_contacts');
|
||||
},
|
||||
|
||||
preview_message: function(frm) {
|
||||
if (frm.doc.message) {
|
||||
frappe.call({
|
||||
method: "frappe.automation.doctype.auto_repeat.auto_repeat.generate_message_preview",
|
||||
args: {
|
||||
reference_dt: frm.doc.reference_doctype,
|
||||
reference_doc: frm.doc.reference_document,
|
||||
subject: frm.doc.subject,
|
||||
message: frm.doc.message
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
frappe.msgprint(r.message.message, r.message.subject)
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
frappe.msgprint(__("Please setup a message first"), __("Message not setup"))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frappe.auto_repeat.render_schedule = function(frm) {
|
||||
if (!frm.is_dirty() && frm.doc.status !== 'Disabled') {
|
||||
frappe.call({
|
||||
method: "get_auto_repeat_schedule",
|
||||
doc: frm.doc
|
||||
}).done((r) => {
|
||||
frm.dashboard.wrapper.empty();
|
||||
frm.dashboard.add_section(
|
||||
frappe.render_template("auto_repeat_schedule", {
|
||||
schedule_details : r.message || []
|
||||
})
|
||||
);
|
||||
frm.dashboard.show();
|
||||
});
|
||||
} else {
|
||||
frm.dashboard.hide();
|
||||
}
|
||||
};
|
||||
239
frappe/automation/doctype/auto_repeat/auto_repeat.json
Normal file
239
frappe/automation/doctype/auto_repeat/auto_repeat.json
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
{
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:AUT-AR-{#####}",
|
||||
"creation": "2018-03-09 11:22:31.192349",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"section_break_1",
|
||||
"disabled",
|
||||
"section_break_3",
|
||||
"reference_doctype",
|
||||
"reference_document",
|
||||
"column_break_5",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"section_break_10",
|
||||
"frequency",
|
||||
"repeat_on_day",
|
||||
"repeat_on_last_day",
|
||||
"column_break_12",
|
||||
"next_schedule_date",
|
||||
"notification",
|
||||
"notify_by_email",
|
||||
"recipients",
|
||||
"get_contacts",
|
||||
"template",
|
||||
"subject",
|
||||
"message",
|
||||
"preview_message",
|
||||
"print_format",
|
||||
"status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "section_break_1",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Document Type",
|
||||
"options": "DocType",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_document",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Reference Document",
|
||||
"no_copy": 1,
|
||||
"options": "reference_doctype",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "Today",
|
||||
"fieldname": "start_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Start Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "end_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "End Date"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_10",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "frequency",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Frequency",
|
||||
"options": "\nDaily\nWeekly\nMonthly\nQuarterly\nHalf-yearly\nYearly",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: in_list([\"Monthly\", \"Quarterly\", \"Half-yearly\", \"Yearly\"], doc.frequency) && !doc.repeat_on_last_day\n",
|
||||
"fieldname": "repeat_on_day",
|
||||
"fieldtype": "Int",
|
||||
"label": "Repeat on Day"
|
||||
},
|
||||
{
|
||||
"fieldname": "next_schedule_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Next Schedule Date",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "notification",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Notification"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "notify_by_email",
|
||||
"fieldtype": "Check",
|
||||
"label": "Notify by Email"
|
||||
},
|
||||
{
|
||||
"depends_on": "notify_by_email",
|
||||
"fieldname": "recipients",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Recipients"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.notify_by_email && doc.reference_doctype && doc.reference_document",
|
||||
"fieldname": "get_contacts",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Contacts"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.notify_by_email",
|
||||
"fieldname": "template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Template",
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.notify_by_email",
|
||||
"description": "To add dynamic subject, use jinja tags like\n\n<div><pre><code>New {{ doc.doctype }} #{{ doc.name }}</code></pre></div>",
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subject"
|
||||
},
|
||||
{
|
||||
"default": "Please find attached {{ doc.doctype }} #{{ doc.name }}",
|
||||
"depends_on": "eval: doc.notify_by_email",
|
||||
"fieldname": "message",
|
||||
"fieldtype": "Text",
|
||||
"label": "Message"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.notify_by_email && doc.reference_doctype && doc.reference_document",
|
||||
"fieldname": "preview_message",
|
||||
"fieldtype": "Button",
|
||||
"label": "Preview Message"
|
||||
},
|
||||
{
|
||||
"depends_on": "notify_by_email",
|
||||
"fieldname": "print_format",
|
||||
"fieldtype": "Link",
|
||||
"label": "Print Format",
|
||||
"options": "Print Format"
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "\nActive\nDisabled\nCompleted",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_3",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.frequency === 'Monthly'",
|
||||
"fieldname": "repeat_on_last_day",
|
||||
"fieldtype": "Check",
|
||||
"label": "Repeat on Last Day of the Month"
|
||||
}
|
||||
],
|
||||
"modified": "2019-07-17 11:30:51.412317",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Auto Repeat",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "reference_document",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "reference_document",
|
||||
"track_changes": 1
|
||||
}
|
||||
374
frappe/automation/doctype/auto_repeat/auto_repeat.py
Normal file
374
frappe/automation/doctype/auto_repeat/auto_repeat.py
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.form import assign_to
|
||||
from frappe.utils.jinja import validate_template
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe.utils.user import get_system_managers
|
||||
from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day
|
||||
from frappe.model.document import Document
|
||||
from frappe.core.doctype.communication.email import make
|
||||
from frappe.utils.background_jobs import get_jobs
|
||||
|
||||
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
|
||||
|
||||
|
||||
class AutoRepeat(Document):
|
||||
def validate(self):
|
||||
self.update_status()
|
||||
self.validate_reference_doctype()
|
||||
self.validate_dates()
|
||||
self.validate_email_id()
|
||||
self.set_dates()
|
||||
self.update_auto_repeat_id()
|
||||
self.unlink_if_applicable()
|
||||
|
||||
validate_template(self.subject or "")
|
||||
validate_template(self.message or "")
|
||||
|
||||
def before_insert(self):
|
||||
if not frappe.flags.in_test:
|
||||
start_date = self.start_date
|
||||
today_date = today()
|
||||
if start_date <= today_date:
|
||||
start_date = today_date
|
||||
|
||||
def after_save(self):
|
||||
frappe.get_doc(self.reference_doctype, self.reference_document).notify_update()
|
||||
|
||||
def on_trash(self):
|
||||
frappe.db.set_value(self.reference_doctype, self.reference_document, {
|
||||
'auto_repeat': self.name
|
||||
}, 'auto_repeat', '')
|
||||
|
||||
def set_dates(self):
|
||||
if self.disabled:
|
||||
self.next_schedule_date = None
|
||||
else:
|
||||
self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, self.end_date)
|
||||
|
||||
def unlink_if_applicable(self):
|
||||
if self.status == 'Completed' or self.disabled:
|
||||
frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '')
|
||||
|
||||
def validate_reference_doctype(self):
|
||||
if not frappe.flags.in_test:
|
||||
if not frappe.get_meta(self.reference_doctype).allow_auto_repeat:
|
||||
frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype))
|
||||
|
||||
def validate_dates(self):
|
||||
self.validate_from_to_dates('start_date', 'end_date')
|
||||
|
||||
if self.end_date == self.start_date:
|
||||
frappe.throw(_('{0} should not be same as {1}').format(frappe.bold('End Date'), frappe.bold('Start Date')))
|
||||
|
||||
def validate_email_id(self):
|
||||
if self.notify_by_email:
|
||||
if self.recipients:
|
||||
email_list = split_emails(self.recipients.replace("\n", ""))
|
||||
from frappe.utils import validate_email_address
|
||||
|
||||
for email in email_list:
|
||||
if not validate_email_address(email):
|
||||
frappe.throw(_("{0} is an invalid email address in 'Recipients'").format(email))
|
||||
else:
|
||||
frappe.throw(_("'Recipients' not specified"))
|
||||
|
||||
def update_auto_repeat_id(self):
|
||||
#check if document is already on auto repeat
|
||||
auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat")
|
||||
if auto_repeat and auto_repeat != self.name:
|
||||
frappe.throw(_("The {0} is already on auto repeat {1}").format(self.reference_document, auto_repeat))
|
||||
else:
|
||||
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name)
|
||||
|
||||
def update_status(self):
|
||||
if self.disabled:
|
||||
self.status = "Disabled"
|
||||
elif self.is_completed():
|
||||
self.status = "Completed"
|
||||
else:
|
||||
self.status = "Active"
|
||||
|
||||
def is_completed(self):
|
||||
return self.end_date and getdate(self.end_date) < getdate(today())
|
||||
|
||||
def get_auto_repeat_schedule(self):
|
||||
schedule_details = []
|
||||
start_date = getdate(self.start_date)
|
||||
end_date = getdate(self.end_date)
|
||||
today = frappe.utils.datetime.date.today()
|
||||
|
||||
if start_date < today:
|
||||
start_date = today
|
||||
|
||||
if not self.end_date:
|
||||
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
|
||||
row = {
|
||||
"reference_document": self.reference_document,
|
||||
"frequency": self.frequency,
|
||||
"next_scheduled_date": start_date
|
||||
}
|
||||
schedule_details.append(row)
|
||||
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
|
||||
|
||||
if self.end_date:
|
||||
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
|
||||
while (getdate(start_date) < getdate(end_date)):
|
||||
row = {
|
||||
"reference_document" : self.reference_document,
|
||||
"frequency" : self.frequency,
|
||||
"next_scheduled_date" : start_date
|
||||
}
|
||||
schedule_details.append(row)
|
||||
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, end_date)
|
||||
|
||||
|
||||
return schedule_details
|
||||
|
||||
def create_documents(self):
|
||||
try:
|
||||
new_doc = self.make_new_document()
|
||||
if self.notify_by_email and self.recipients:
|
||||
self.send_notification(new_doc)
|
||||
except Exception:
|
||||
error_log = frappe.log_error(frappe.get_traceback(), _("Auto Repeat Document Creation Failure"))
|
||||
|
||||
self.disable_auto_repeat()
|
||||
|
||||
if self.reference_document and not frappe.flags.in_test:
|
||||
self.notify_error_to_user(error_log)
|
||||
|
||||
def make_new_document(self):
|
||||
reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document)
|
||||
new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False)
|
||||
self.update_doc(new_doc, reference_doc)
|
||||
new_doc.insert(ignore_permissions = True)
|
||||
|
||||
return new_doc
|
||||
|
||||
def update_doc(self, new_doc, reference_doc):
|
||||
new_doc.docstatus = 0
|
||||
if new_doc.meta.get_field('set_posting_time'):
|
||||
new_doc.set('set_posting_time', 1)
|
||||
|
||||
if new_doc.meta.get_field('auto_repeat'):
|
||||
new_doc.set('auto_repeat', self.name)
|
||||
|
||||
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'remarks', 'owner']:
|
||||
if new_doc.meta.get_field(fieldname):
|
||||
new_doc.set(fieldname, reference_doc.get(fieldname))
|
||||
|
||||
for data in new_doc.meta.fields:
|
||||
if data.fieldtype == 'Date' and data.reqd:
|
||||
new_doc.set(data.fieldname, self.next_schedule_date)
|
||||
|
||||
self.set_auto_repeat_period(new_doc)
|
||||
|
||||
auto_repeat_doc = frappe.get_doc('Auto Repeat', self.name)
|
||||
|
||||
#for any action that needs to take place after the recurring document creation
|
||||
#on recurring method of that doctype is triggered
|
||||
new_doc.run_method('on_recurring', reference_doc = reference_doc, auto_repeat_doc = auto_repeat_doc)
|
||||
|
||||
def set_auto_repeat_period(self, new_doc):
|
||||
mcount = month_map.get(self.frequency)
|
||||
if mcount and new_doc.meta.get_field('from_date') and new_doc.meta.get_field('to_date'):
|
||||
last_ref_doc = frappe.db.get_all(doctype = self.reference_doctype,
|
||||
fields = ['name', 'from_date', 'to_date'],
|
||||
filters = [
|
||||
['auto_repeat', '=', self.name],
|
||||
['docstatus', '<', 2],
|
||||
],
|
||||
order_by = 'creation desc',
|
||||
limit = 1)
|
||||
|
||||
if not last_ref_doc:
|
||||
return
|
||||
|
||||
from_date = get_next_date(last_ref_doc[0].from_date, mcount)
|
||||
|
||||
if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and \
|
||||
(cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date)):
|
||||
to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount))
|
||||
else:
|
||||
to_date = get_next_date(last_ref_doc[0].to_date, mcount)
|
||||
|
||||
new_doc.set('from_date', from_date)
|
||||
new_doc.set('to_date', to_date)
|
||||
|
||||
def send_notification(self, new_doc):
|
||||
"""Notify concerned people about recurring document generation"""
|
||||
subject = self.subject or ''
|
||||
message = self.message or ''
|
||||
|
||||
if not self.subject:
|
||||
subject = _("New {0}: {1}").format(new_doc.doctype, new_doc.name)
|
||||
elif "{" in self.subject:
|
||||
subject = frappe.render_template(self.subject, {'doc': new_doc})
|
||||
|
||||
if not self.message:
|
||||
message = _("Please find attached {0}: {1}").format(new_doc.doctype, new_doc.name)
|
||||
elif "{" in self.message:
|
||||
message = frappe.render_template(self.message, {'doc': new_doc})
|
||||
|
||||
print_format = self.print_format or 'Standard'
|
||||
|
||||
attachments = [frappe.attach_print(new_doc.doctype, new_doc.name,
|
||||
file_name=new_doc.name, print_format=print_format)]
|
||||
|
||||
make(doctype=new_doc.doctype, name=new_doc.name, recipients=self.recipients,
|
||||
subject=subject, content=message, attachments=attachments, send_email=1)
|
||||
|
||||
def fetch_linked_contacts(self):
|
||||
if self.reference_doctype and self.reference_document:
|
||||
res = frappe.db.get_all('Contact',
|
||||
fields=['email_id'],
|
||||
filters=[
|
||||
['Dynamic Link', 'link_doctype', '=', self.reference_doctype],
|
||||
['Dynamic Link', 'link_name', '=', self.reference_document]
|
||||
])
|
||||
|
||||
email_ids = list(set([d.email_id for d in res]))
|
||||
if not email_ids:
|
||||
frappe.msgprint(_('No contacts linked to document'), alert=True)
|
||||
else:
|
||||
self.recipients = ', '.join(email_ids)
|
||||
|
||||
def disable_auto_repeat(self):
|
||||
frappe.db.set_value('Auto Repeat', self.name, 'disabled', 1)
|
||||
|
||||
def notify_error_to_user(self, error_log):
|
||||
recipients = get_system_managers(only_name=True) + self.owner
|
||||
subject = _("Auto Repeat Document Creation Failed")
|
||||
|
||||
form_link = frappe.utils.get_link_to_form(self.reference_doctype, self.reference_document)
|
||||
auto_repeat_failed_for = _('Auto Repeat failed for {0}').format(form_link)
|
||||
|
||||
error_log_link =frappe.utils.get_link_to_form(error_log.reference_doctype, error_log.reference_document)
|
||||
error_log_message = _('Check the Error Log for more information: {0}').format(error_log_link)
|
||||
|
||||
frappe.sendmail(
|
||||
recipients=recipients,
|
||||
subject=subject,
|
||||
template="auto_repeat_fail",
|
||||
args={
|
||||
'auto_repeat_failed_for': auto_repeat_failed_for,
|
||||
'error_log_message': error_log_message
|
||||
},
|
||||
header=[subject, 'red']
|
||||
)
|
||||
|
||||
|
||||
def get_next_schedule_date(start_date, frequency, repeat_on_day, repeat_on_last_day = False, end_date = None):
|
||||
month_count = month_map.get(frequency)
|
||||
if month_count and repeat_on_last_day:
|
||||
next_date = get_next_date(start_date, month_count, 31)
|
||||
elif month_count and repeat_on_day:
|
||||
next_date = get_next_date(start_date, month_count, repeat_on_day)
|
||||
elif month_count:
|
||||
next_date = get_next_date(start_date, month_count)
|
||||
else:
|
||||
days = 7 if frequency == 'Weekly' else 1
|
||||
next_date = add_days(start_date, days)
|
||||
|
||||
return next_date
|
||||
|
||||
def get_next_date(dt, mcount, day=None):
|
||||
dt = getdate(dt)
|
||||
dt += relativedelta(months=mcount, day=day)
|
||||
return dt
|
||||
|
||||
#called through hooks
|
||||
def make_auto_repeat_entry():
|
||||
enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries'
|
||||
jobs = get_jobs()
|
||||
|
||||
if not jobs or enqueued_method not in jobs[frappe.local.site]:
|
||||
date = getdate(today())
|
||||
data = get_auto_repeat_entries(date)
|
||||
frappe.enqueue(enqueued_method, data=data)
|
||||
|
||||
def create_repeated_entries(data):
|
||||
for d in data:
|
||||
doc = frappe.get_doc('Auto Repeat', d.name)
|
||||
|
||||
current_date = getdate(today())
|
||||
schedule_date = getdate(doc.next_schedule_date)
|
||||
|
||||
while schedule_date <= current_date and not doc.disabled:
|
||||
doc.create_documents()
|
||||
schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date)
|
||||
|
||||
if schedule_date and not doc.disabled:
|
||||
frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date)
|
||||
|
||||
def get_auto_repeat_entries(date=None):
|
||||
if not date:
|
||||
date = getdate(today())
|
||||
return frappe.db.get_all('Auto Repeat', filters=[
|
||||
['next_schedule_date', '<=', date],
|
||||
['status', '=', 'Active']
|
||||
])
|
||||
|
||||
#called through hooks
|
||||
def set_auto_repeat_as_completed():
|
||||
auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']})
|
||||
for entry in auto_repeat:
|
||||
doc = frappe.get_doc("Auto Repeat", entry.name)
|
||||
if doc.is_completed():
|
||||
doc.status = 'Completed'
|
||||
doc.save()
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_auto_repeat(doctype, docname, frequency, start_date, end_date = None):
|
||||
doc = frappe.new_doc('Auto Repeat')
|
||||
doc.reference_doctype = doctype
|
||||
doc.reference_document = docname
|
||||
doc.frequency = frequency
|
||||
doc.start_date = start_date
|
||||
if end_date:
|
||||
doc.end_date = end_date
|
||||
doc.save()
|
||||
return doc
|
||||
|
||||
#method for reference_doctype filter
|
||||
def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters):
|
||||
res = frappe.db.get_all('Property Setter', {
|
||||
'property': 'allow_auto_repeat',
|
||||
'value': '1',
|
||||
}, ['doc_type'])
|
||||
docs = [r.doc_type for r in res]
|
||||
|
||||
res = frappe.db.get_all('DocType', {
|
||||
'allow_auto_repeat': 1,
|
||||
}, ['name'])
|
||||
docs += [r.name for r in res]
|
||||
docs = set(list(docs))
|
||||
|
||||
return [[d] for d in docs]
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_reference(docname, reference):
|
||||
result = ""
|
||||
try:
|
||||
frappe.db.set_value("Auto Repeat", docname, "reference_document", reference)
|
||||
result = "success"
|
||||
except Exception as e:
|
||||
result = "error"
|
||||
raise e
|
||||
return result
|
||||
|
||||
@frappe.whitelist()
|
||||
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
|
||||
doc = frappe.get_doc(reference_dt, reference_doc)
|
||||
subject_preview = _("Please add a subject to your email")
|
||||
msg_preview = frappe.render_template(message, {'doc': doc})
|
||||
if subject:
|
||||
subject_preview = frappe.render_template(subject, {'doc': doc})
|
||||
|
||||
return {'message': msg_preview, 'subject': subject_preview}
|
||||
11
frappe/automation/doctype/auto_repeat/auto_repeat_list.js
Normal file
11
frappe/automation/doctype/auto_repeat/auto_repeat_list.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
frappe.listview_settings['Auto Repeat'] = {
|
||||
add_fields: ["next_schedule_date"],
|
||||
get_indicator: function(doc) {
|
||||
var colors = {
|
||||
"Active": "green",
|
||||
"Disabled": "red",
|
||||
"Completed": "blue",
|
||||
};
|
||||
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
|
||||
}
|
||||
};
|
||||
|
|
@ -11,10 +11,9 @@
|
|||
{% for(var i=0; i < schedule_details.length; i++) { %}
|
||||
<tr>
|
||||
<td>{{ schedule_details[i].reference_document }}</td>
|
||||
<td> {{ schedule_details[i].frequency }} </td>
|
||||
<td> {{ schedule_details[i].next_scheduled_date }} </td>
|
||||
<td> {{ __(schedule_details[i].frequency) }} </td>
|
||||
<td> {{ frappe.datetime.str_to_user(schedule_details[i].next_scheduled_date) }} </td>
|
||||
</tr>
|
||||
{% } %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
|
@ -7,20 +7,19 @@ import unittest
|
|||
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
from frappe.desk.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, disable_auto_repeat
|
||||
from frappe.utils import today, add_days, getdate
|
||||
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries
|
||||
from frappe.utils import today, add_days, getdate, add_months
|
||||
|
||||
|
||||
def add_custom_fields():
|
||||
df = dict(
|
||||
fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender',
|
||||
options='Auto Repeat')
|
||||
options='Auto Repeat', hidden=1, print_hide=1, read_only=1)
|
||||
create_custom_field('ToDo', df)
|
||||
|
||||
|
||||
class TestAutoRepeat(unittest.TestCase):
|
||||
def setUp(self):
|
||||
if not frappe.db.sql("SELECT `name` FROM `tabCustom Field` WHERE `name`='auto_repeat'"):
|
||||
if not frappe.db.sql("SELECT `fieldname` FROM `tabCustom Field` WHERE `fieldname`='auto_repeat' and `dt`=%s", "Todo"):
|
||||
add_custom_fields()
|
||||
|
||||
def test_daily_auto_repeat(self):
|
||||
|
|
@ -29,8 +28,8 @@ class TestAutoRepeat(unittest.TestCase):
|
|||
|
||||
doc = make_auto_repeat(reference_document=todo.name)
|
||||
self.assertEqual(doc.next_schedule_date, today())
|
||||
for data in get_auto_repeat_entries(today()):
|
||||
create_repeated_entries(data)
|
||||
data = get_auto_repeat_entries(getdate(today()))
|
||||
create_repeated_entries(data)
|
||||
frappe.db.commit()
|
||||
|
||||
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
|
||||
|
|
@ -44,15 +43,18 @@ class TestAutoRepeat(unittest.TestCase):
|
|||
self.assertEqual(todo.get('description'), new_todo.get('description'))
|
||||
|
||||
def test_monthly_auto_repeat(self):
|
||||
start_date = '2018-01-01'
|
||||
end_date = '2018-12-31'
|
||||
start_date = today()
|
||||
end_date = add_months(start_date, 12)
|
||||
|
||||
todo = frappe.get_doc(
|
||||
dict(doctype='ToDo', description='test recurring todo', assigned_by='Administrator')).insert()
|
||||
|
||||
self.monthly_auto_repeat('ToDo', todo.name, start_date, end_date)
|
||||
#test without end_date
|
||||
todo = frappe.get_doc(dict(doctype='ToDo', description='test recurring todo without end_date', assigned_by='Administrator')).insert()
|
||||
self.monthly_auto_repeat('ToDo', todo.name, start_date)
|
||||
|
||||
def monthly_auto_repeat(self, doctype, docname, start_date, end_date):
|
||||
def monthly_auto_repeat(self, doctype, docname, start_date, end_date = None):
|
||||
def get_months(start, end):
|
||||
diff = (12 * end.year + end.month) - (12 * start.year + start.month)
|
||||
return diff + 1
|
||||
|
|
@ -61,10 +63,10 @@ class TestAutoRepeat(unittest.TestCase):
|
|||
reference_doctype=doctype, frequency='Monthly', reference_document=docname, start_date=start_date,
|
||||
end_date=end_date)
|
||||
|
||||
disable_auto_repeat(doc)
|
||||
doc.disable_auto_repeat()
|
||||
|
||||
for data in get_auto_repeat_entries(today()):
|
||||
create_repeated_entries(data)
|
||||
data = get_auto_repeat_entries(getdate(today()))
|
||||
create_repeated_entries(data)
|
||||
docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name})
|
||||
self.assertEqual(len(docnames), 1)
|
||||
|
||||
|
|
@ -72,8 +74,8 @@ class TestAutoRepeat(unittest.TestCase):
|
|||
doc.db_set('disabled', 0)
|
||||
|
||||
months = get_months(getdate(start_date), getdate(today()))
|
||||
for data in get_auto_repeat_entries(today()):
|
||||
create_repeated_entries(data)
|
||||
data = get_auto_repeat_entries(getdate(today()))
|
||||
create_repeated_entries(data)
|
||||
|
||||
docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name})
|
||||
self.assertEqual(len(docnames), months)
|
||||
|
|
@ -84,8 +86,8 @@ class TestAutoRepeat(unittest.TestCase):
|
|||
|
||||
doc = make_auto_repeat(reference_document=todo.name, notify=1, recipients="test@domain.com", subject="New ToDo",
|
||||
message="A new ToDo has just been created for you")
|
||||
for data in get_auto_repeat_entries(today()):
|
||||
create_repeated_entries(data)
|
||||
data = get_auto_repeat_entries(getdate(today()))
|
||||
create_repeated_entries(data)
|
||||
frappe.db.commit()
|
||||
|
||||
new_todo = frappe.db.get_value('ToDo',
|
||||
|
|
@ -100,18 +102,14 @@ def make_auto_repeat(**args):
|
|||
doc = frappe.get_doc({
|
||||
'doctype': 'Auto Repeat',
|
||||
'reference_doctype': args.reference_doctype or 'ToDo',
|
||||
'reference_document': args.reference_document or frappe.db.get_value('ToDo', {'docstatus': 1}, 'name'),
|
||||
'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'),
|
||||
'frequency': args.frequency or 'Daily',
|
||||
'start_date': args.start_date or add_days(today(), -1),
|
||||
'end_date': args.end_date or add_days(today(), 1),
|
||||
'submit_on_creation': args.submit_on_creation or 0,
|
||||
'end_date': args.end_date or "",
|
||||
'notify_by_email': args.notify or 0,
|
||||
'recipients': args.recipients or "",
|
||||
'subject': args.subject or "",
|
||||
'message': args.message or ""
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
if not args.do_not_submit:
|
||||
doc.submit()
|
||||
|
||||
return doc
|
||||
|
|
@ -72,7 +72,7 @@ def get_bootinfo():
|
|||
bootinfo.lang = text_type(bootinfo.lang)
|
||||
bootinfo.versions = {k: v['version'] for k, v in get_versions().items()}
|
||||
|
||||
bootinfo.error_report_email = frappe.get_hooks("error_report_email")
|
||||
bootinfo.error_report_email = frappe.conf.error_report_email
|
||||
bootinfo.calendars = sorted(frappe.get_hooks("calendars"))
|
||||
bootinfo.treeviews = frappe.get_hooks("treeviews") or []
|
||||
bootinfo.lang_dict = get_lang_dict()
|
||||
|
|
|
|||
|
|
@ -361,6 +361,10 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder
|
|||
|
||||
return _file.as_dict()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_hooks(hook, app_name=None):
|
||||
return frappe.get_hooks(hook, app_name)
|
||||
|
||||
def check_parent_permission(parent, child_doctype):
|
||||
if parent:
|
||||
# User may pass fake parent and get the information from the child table
|
||||
|
|
|
|||
|
|
@ -72,12 +72,11 @@ def call_command(cmd, context):
|
|||
|
||||
def get_commands():
|
||||
# prevent circular imports
|
||||
from .docs import commands as doc_commands
|
||||
from .scheduler import commands as scheduler_commands
|
||||
from .site import commands as site_commands
|
||||
from .translate import commands as translate_commands
|
||||
from .utils import commands as utils_commands
|
||||
|
||||
return list(set(doc_commands + scheduler_commands + site_commands + translate_commands + utils_commands))
|
||||
return list(set(scheduler_commands + site_commands + translate_commands + utils_commands))
|
||||
|
||||
commands = get_commands()
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
from __future__ import unicode_literals, absolute_import
|
||||
import click
|
||||
import os, shutil
|
||||
import frappe
|
||||
from frappe.commands import pass_context
|
||||
|
||||
@click.command('build-docs')
|
||||
@pass_context
|
||||
@click.argument('app')
|
||||
@click.option('--docs-version', default='current')
|
||||
@click.option('--target', default=None)
|
||||
@click.option('--local', default=False, is_flag=True, help='Run app locally')
|
||||
@click.option('--watch', default=False, is_flag=True, help='Watch for changes and rewrite')
|
||||
def build_docs(context, app, docs_version="current", target=None, local=False, watch=False):
|
||||
"Setup docs in target folder of target app"
|
||||
from frappe.utils import watch as start_watch
|
||||
from frappe.utils.setup_docs import add_breadcrumbs_tag
|
||||
|
||||
for site in context.sites:
|
||||
_build_docs_once(site, app, docs_version, target, local)
|
||||
|
||||
if watch:
|
||||
def trigger_make(source_path, event_type):
|
||||
if "/docs/user/" in source_path:
|
||||
# user file
|
||||
target_path = frappe.get_app_path(target, 'www', 'docs', 'user',
|
||||
os.path.relpath(source_path, start=frappe.get_app_path(app, 'docs', 'user')))
|
||||
shutil.copy(source_path, target_path)
|
||||
add_breadcrumbs_tag(target_path)
|
||||
|
||||
if source_path.endswith('/docs/index.md'):
|
||||
target_path = frappe.get_app_path(target, 'www', 'docs', 'index.md')
|
||||
shutil.copy(source_path, target_path)
|
||||
|
||||
apps_path = frappe.get_app_path(app)
|
||||
start_watch(apps_path, handler=trigger_make)
|
||||
|
||||
def _build_docs_once(site, app, docs_version, target, local, only_content_updated=False):
|
||||
from frappe.utils.setup_docs import setup_docs
|
||||
|
||||
try:
|
||||
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
make = setup_docs(app, target)
|
||||
|
||||
if not only_content_updated:
|
||||
make.build(docs_version)
|
||||
|
||||
#make.make_docs(target, local)
|
||||
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
commands = [
|
||||
build_docs,
|
||||
]
|
||||
|
|
@ -157,16 +157,17 @@ def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_ro
|
|||
admin_password=admin_password)
|
||||
|
||||
@click.command('install-app')
|
||||
@click.argument('app')
|
||||
@click.argument('apps', nargs=-1)
|
||||
@pass_context
|
||||
def install_app(context, app):
|
||||
"Install a new app to site"
|
||||
def install_app(context, apps):
|
||||
"Install a new app to site, supports multiple apps"
|
||||
from frappe.installer import install_app as _install_app
|
||||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
try:
|
||||
_install_app(app, verbose=context.verbose)
|
||||
for app in apps:
|
||||
_install_app(app, verbose=context.verbose)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
|
|
@ -229,6 +230,7 @@ def migrate(context, rebuild_website=False):
|
|||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
print("Compiling Python Files...")
|
||||
compileall.compile_dir('../apps', quiet=1, rx=re.compile('.*node_modules.*'))
|
||||
|
||||
@click.command('run-patch')
|
||||
|
|
@ -562,10 +564,7 @@ def browse(context, site):
|
|||
site = site.lower()
|
||||
|
||||
if site in frappe.utils.get_sites():
|
||||
webbrowser.open('http://{site}:{port}'.format(
|
||||
site=site,
|
||||
port=frappe.get_conf(site).webserver_port
|
||||
), new=2)
|
||||
webbrowser.open(frappe.utils.get_site_url(site), new=2)
|
||||
else:
|
||||
click.echo("\nSite named \033[1m{}\033[0m doesn't exist\n".format(site))
|
||||
|
||||
|
|
|
|||
|
|
@ -441,7 +441,7 @@ def run_tests(context, app=None, module=None, doctype=None, test=(),
|
|||
if coverage:
|
||||
# Generate coverage report only for app that is being tested
|
||||
source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
|
||||
cov = Coverage(source=[source_path], omit=['*.html', '*.js', '*.css'])
|
||||
cov = Coverage(source=[source_path], omit=['*.html', '*.js', '*.xml', '*.css', '*/doctype/*/*_dashboard.py', '*/patches/*'])
|
||||
cov.start()
|
||||
|
||||
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
|
||||
|
|
@ -459,26 +459,26 @@ def run_tests(context, app=None, module=None, doctype=None, test=(),
|
|||
sys.exit(ret)
|
||||
|
||||
@click.command('run-ui-tests')
|
||||
@click.option('--app', help="App to run tests on, leave blank for all apps")
|
||||
@click.option('--test', help="Path to the specific test you want to run")
|
||||
@click.option('--test-list', help="Path to the txt file with the list of test cases")
|
||||
@click.option('--profile', is_flag=True, default=False)
|
||||
@click.argument('app')
|
||||
@click.option('--headless', is_flag=True, help="Run UI Test in headless mode")
|
||||
@pass_context
|
||||
def run_ui_tests(context, app=None, test=False, test_list=False, profile=False):
|
||||
def run_ui_tests(context, app, headless=False):
|
||||
"Run UI tests"
|
||||
import frappe.test_runner
|
||||
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
app_base_path = os.path.abspath(os.path.join(frappe.get_app_path(app), '..'))
|
||||
site_url = frappe.utils.get_site_url(site)
|
||||
admin_password = frappe.get_conf(site).admin_password
|
||||
|
||||
ret = frappe.test_runner.run_ui_tests(app=app, test=test, test_list=test_list, verbose=context.verbose,
|
||||
profile=profile)
|
||||
if len(ret.failures) == 0 and len(ret.errors) == 0:
|
||||
ret = 0
|
||||
# override baseUrl using env variable
|
||||
site_env = 'CYPRESS_baseUrl={}'.format(site_url)
|
||||
password_env = 'CYPRESS_adminPassword={}'.format(admin_password) if admin_password else ''
|
||||
|
||||
if os.environ.get('CI'):
|
||||
sys.exit(ret)
|
||||
# run for headless mode
|
||||
run_or_open = 'run' if headless else 'open'
|
||||
command = '{site_env} {password_env} yarn run cypress {run_or_open}'
|
||||
formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open)
|
||||
frappe.commands.popen(formatted_command, cwd=app_base_path)
|
||||
|
||||
@click.command('run-setup-wizard-ui-test')
|
||||
@click.option('--app', help="App to run tests on, leave blank for all apps")
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ def get_data():
|
|||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Google Maps Settings",
|
||||
"description": _("Google Maps integration"),
|
||||
"name": "Google Settings",
|
||||
"description": _("Google API Settings."),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
|
|
@ -111,6 +111,11 @@ def get_data():
|
|||
"type": "doctype",
|
||||
"name": "GSuite Templates",
|
||||
"description": _("Google GSuite Templates to integration with DocTypes"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Google Contacts",
|
||||
"description": _("Google Contacts Integration."),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ def get_data():
|
|||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Blog Settings",
|
||||
"description": _("Write titles and introductions to your blog."),
|
||||
"name": "Blogger",
|
||||
"description": _("A user who posts blogs."),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -161,3 +161,16 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters):
|
|||
'link_name': link_name,
|
||||
'link_doctype': link_doctype
|
||||
})
|
||||
|
||||
|
||||
def get_contact_with_phone_number(number):
|
||||
if not number: return
|
||||
|
||||
contacts = frappe.get_all('Contact', or_filters={
|
||||
'phone': ['like', '%{}'.format(number)],
|
||||
'mobile_no': ['like', '%{}'.format(number)]
|
||||
}, limit=1)
|
||||
|
||||
contact = contacts[0].name if contacts else None
|
||||
|
||||
return contact
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
from __future__ import unicode_literals
|
||||
from six import iteritems
|
||||
import frappe
|
||||
|
||||
from frappe import _
|
||||
|
||||
field_map = {
|
||||
"Contact": [ "first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact" ],
|
||||
|
|
@ -94,6 +94,9 @@ def get_reference_details(reference_doctype, doctype, reference_list, reference_
|
|||
for d in records:
|
||||
temp_records.append(d[1:])
|
||||
|
||||
if not reference_list:
|
||||
frappe.throw(_("No records present in {0}".format(reference_doctype)))
|
||||
|
||||
reference_details[reference_list[0]][frappe.scrub(doctype)] = temp_records
|
||||
return reference_details
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
|||
from frappe import _
|
||||
from frappe.utils import get_fullname, now
|
||||
from frappe.model.document import Document
|
||||
from frappe.core.utils import get_parent_doc, set_timeline_doc
|
||||
from frappe.core.utils import set_timeline_doc
|
||||
import frappe
|
||||
|
||||
class ActivityLog(Document):
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ def get_feed_match_conditions(user=None, doctype='Comment'):
|
|||
user_permissions = frappe.permissions.get_user_permissions(user)
|
||||
can_read = frappe.get_user().get_can_read()
|
||||
|
||||
can_read_doctypes = ["'{}'".format(doctype) for doctype in
|
||||
can_read_doctypes = ["'{}'".format(dt) for dt in
|
||||
list(set(can_read) - set(list(user_permissions)))]
|
||||
|
||||
if can_read_doctypes:
|
||||
|
|
|
|||
|
|
@ -48,10 +48,10 @@ class TestComment(unittest.TestCase):
|
|||
add_comment('pleez vizits my site http://mysite.com', 'test@test.com', 'bad commentor',
|
||||
'Blog Post', test_blog.name, test_blog.route)
|
||||
|
||||
self.assertEqual(frappe.get_all('Comment', fields = ['*'], filters = dict(
|
||||
self.assertEqual(len(frappe.get_all('Comment', fields = ['*'], filters = dict(
|
||||
reference_doctype = test_blog.doctype,
|
||||
reference_name = test_blog.name
|
||||
))[0].published, 0)
|
||||
))), 0)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"allow_import": 1,
|
||||
"creation": "2013-01-29 10:47:14",
|
||||
"description": "Keep a track of all communications",
|
||||
"description": "Keeps track of all communications",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
|
|
@ -41,14 +41,11 @@
|
|||
"user",
|
||||
"column_break_27",
|
||||
"email_template",
|
||||
"link_doctype",
|
||||
"link_name",
|
||||
"timeline_doctype",
|
||||
"timeline_name",
|
||||
"timeline_label",
|
||||
"unread_notification_sent",
|
||||
"seen",
|
||||
"_user_tags",
|
||||
"timeline_links_sections",
|
||||
"timeline_links",
|
||||
"email_inbox",
|
||||
"message_id",
|
||||
"uid",
|
||||
|
|
@ -204,6 +201,7 @@
|
|||
"label": "Date"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "read_receipt",
|
||||
"fieldtype": "Check",
|
||||
"label": "Sent Read Receipt",
|
||||
|
|
@ -220,6 +218,7 @@
|
|||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "read_by_recipient",
|
||||
"fieldtype": "Check",
|
||||
"label": "Read by Recipient",
|
||||
|
|
@ -284,39 +283,6 @@
|
|||
"fieldname": "column_break_27",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "link_doctype",
|
||||
"fieldtype": "Link",
|
||||
"label": "Link DocType",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "link_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Link Name",
|
||||
"options": "link_doctype",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "timeline_doctype",
|
||||
"fieldtype": "Link",
|
||||
"label": "Timeline DocType",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "timeline_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Timeline Name",
|
||||
"options": "timeline_doctype",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "timeline_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Timeline field Name"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "unread_notification_sent",
|
||||
|
|
@ -325,6 +291,7 @@
|
|||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "seen",
|
||||
"fieldtype": "Check",
|
||||
"label": "Seen",
|
||||
|
|
@ -368,6 +335,7 @@
|
|||
"options": "Open\nSpam\nTrash"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "has_attachment",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
|
|
@ -398,11 +366,24 @@
|
|||
"label": "Email Template",
|
||||
"options": "Email Template",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "timeline_links_sections",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Timeline Links"
|
||||
},
|
||||
{
|
||||
"fieldname": "timeline_links",
|
||||
"fieldtype": "Table",
|
||||
"label": "Timeline Links",
|
||||
"options": "Communication Link",
|
||||
"permlevel": 2
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-comment",
|
||||
"idx": 1,
|
||||
"modified": "2019-05-04 15:36:35.818714",
|
||||
"modified": "2019-05-21 09:48:24.892143",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Communication",
|
||||
|
|
@ -428,6 +409,18 @@
|
|||
"role": "System Manager",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"permlevel": 2,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
|
|
@ -437,6 +430,7 @@
|
|||
}
|
||||
],
|
||||
"search_fields": "subject",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "subject",
|
||||
"track_changes": 1,
|
||||
|
|
|
|||
|
|
@ -8,11 +8,12 @@ from frappe.model.document import Document
|
|||
from frappe.utils import validate_email_address, get_fullname, strip_html, cstr
|
||||
from frappe.core.doctype.communication.email import (validate_email,
|
||||
notify, _notify, update_parent_mins_to_first_response)
|
||||
from frappe.core.utils import get_parent_doc, set_timeline_doc
|
||||
from frappe.core.utils import get_parent_doc
|
||||
from frappe.utils.bot import BotReply
|
||||
from frappe.utils import parse_addr
|
||||
from frappe.core.doctype.comment.comment import update_comment_in_doc
|
||||
|
||||
from email.utils import parseaddr
|
||||
from six.moves.urllib.parse import unquote
|
||||
from collections import Counter
|
||||
|
||||
exclude_from_linked_with = True
|
||||
|
|
@ -55,10 +56,15 @@ class Communication(Document):
|
|||
self.sent_or_received = "Sent"
|
||||
|
||||
self.set_status()
|
||||
self.set_sender_full_name()
|
||||
|
||||
validate_email(self)
|
||||
set_timeline_doc(self)
|
||||
|
||||
if self.communication_medium == "Email":
|
||||
self.parse_email_for_timeline_links()
|
||||
self.set_timeline_links()
|
||||
self.deduplicate_timeline_links()
|
||||
|
||||
self.set_sender_full_name()
|
||||
|
||||
def validate_reference(self):
|
||||
if self.reference_doctype and self.reference_name:
|
||||
|
|
@ -79,6 +85,7 @@ class Communication(Document):
|
|||
circular_linking = True
|
||||
break
|
||||
doc = get_parent_doc(doc)
|
||||
|
||||
if circular_linking:
|
||||
frappe.throw(_("Please make sure the Reference Communication Docs are not circularly linked."), frappe.CircularLinkingError)
|
||||
|
||||
|
|
@ -154,7 +161,7 @@ class Communication(Document):
|
|||
if sender_name == sender_email:
|
||||
sender_name = None
|
||||
self.sender = sender_email
|
||||
self.sender_full_name = sender_name or get_fullname(frappe.session.user) if frappe.session.user!='Administrator' else None
|
||||
self.sender_full_name = sender_name or frappe.db.exists("Contact", {"email_id": sender_email}) or sender_email
|
||||
|
||||
def send(self, print_html=None, print_format=None, attachments=None,
|
||||
send_me_a_copy=False, recipients=None):
|
||||
|
|
@ -231,26 +238,69 @@ class Communication(Document):
|
|||
if commit:
|
||||
frappe.db.commit()
|
||||
|
||||
def parse_email_for_timeline_links(self):
|
||||
parse_email(self, [self.recipients, self.cc, self.bcc])
|
||||
|
||||
# Timeline Links
|
||||
def set_timeline_links(self):
|
||||
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc])
|
||||
for contact_name in contacts:
|
||||
self.add_link('Contact', contact_name)
|
||||
|
||||
#link contact's dynamic links to communication
|
||||
add_contact_links_to_communication(self, contact_name)
|
||||
|
||||
def deduplicate_timeline_links(self):
|
||||
if self.timeline_links:
|
||||
links, duplicate = [], False
|
||||
|
||||
for l in self.timeline_links:
|
||||
t = (l.link_doctype, l.link_name)
|
||||
if not t in links:
|
||||
links.append(t)
|
||||
else:
|
||||
duplicate = True
|
||||
|
||||
if duplicate:
|
||||
del self.timeline_links[:] # make it python 2 compatible as list.clear() is python 3 only
|
||||
for l in links:
|
||||
self.add_link(link_doctype=l[0], link_name=l[1])
|
||||
|
||||
def add_link(self, link_doctype, link_name, autosave=False):
|
||||
self.append("timeline_links",
|
||||
{
|
||||
"link_doctype": link_doctype,
|
||||
"link_name": link_name
|
||||
}
|
||||
)
|
||||
|
||||
if autosave:
|
||||
self.save(ignore_permissions=True)
|
||||
|
||||
def get_links(self):
|
||||
return self.timeline_links
|
||||
|
||||
def remove_link(self, link_doctype, link_name, autosave=False, ignore_permissions=True):
|
||||
for l in self.timeline_links:
|
||||
if l.link_doctype == link_doctype and l.link_name == link_name:
|
||||
self.timeline_links.remove(l)
|
||||
|
||||
if autosave:
|
||||
self.save(ignore_permissions=ignore_permissions)
|
||||
|
||||
def on_doctype_update():
|
||||
"""Add indexes in `tabCommunication`"""
|
||||
frappe.db.add_index("Communication", ["reference_doctype", "reference_name"])
|
||||
frappe.db.add_index("Communication", ["timeline_doctype", "timeline_name"])
|
||||
frappe.db.add_index("Communication", ["link_doctype", "link_name"])
|
||||
frappe.db.add_index("Communication", ["status", "communication_type"])
|
||||
|
||||
def has_permission(doc, ptype, user):
|
||||
if ptype=="read":
|
||||
if (doc.reference_doctype == "Communication" and doc.reference_name == doc.name) \
|
||||
or (doc.timeline_doctype == "Communication" and doc.timeline_name == doc.name):
|
||||
return
|
||||
if doc.reference_doctype == "Communication" and doc.reference_name == doc.name:
|
||||
return
|
||||
|
||||
if doc.reference_doctype and doc.reference_name:
|
||||
if frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name):
|
||||
return True
|
||||
if doc.timeline_doctype and doc.timeline_name:
|
||||
if frappe.has_permission(doc.timeline_doctype, ptype="read", doc=doc.timeline_name):
|
||||
return True
|
||||
|
||||
def get_permission_query_conditions_for_communication(user):
|
||||
if not user: user = frappe.session.user
|
||||
|
|
@ -265,8 +315,76 @@ def get_permission_query_conditions_for_communication(user):
|
|||
distinct=True, order_by="idx")
|
||||
|
||||
if not accounts:
|
||||
return """tabCommunication.communication_medium!='Email'"""
|
||||
return """`tabCommunication`.communication_medium!='Email'"""
|
||||
|
||||
email_accounts = [ '"%s"'%account.get("email_account") for account in accounts ]
|
||||
return """tabCommunication.email_account in ({email_accounts})"""\
|
||||
return """`tabCommunication`.email_account in ({email_accounts})"""\
|
||||
.format(email_accounts=','.join(email_accounts))
|
||||
|
||||
def get_contacts(email_strings):
|
||||
email_addrs = []
|
||||
|
||||
for email_string in email_strings:
|
||||
if email_string:
|
||||
for email in email_string.split(","):
|
||||
parsed_email = parseaddr(email)[1]
|
||||
if parsed_email:
|
||||
email_addrs.append(parsed_email)
|
||||
|
||||
contacts = []
|
||||
for email in email_addrs:
|
||||
email = get_email_without_link(email)
|
||||
contact_name = frappe.db.get_value('Contact', {'email_id': email})
|
||||
|
||||
if not contact_name:
|
||||
contact = frappe.get_doc({
|
||||
"doctype": "Contact",
|
||||
"first_name": frappe.unscrub(email.split("@")[0]),
|
||||
"email_id": email
|
||||
}).insert(ignore_permissions=True)
|
||||
contact_name = contact.name
|
||||
|
||||
contacts.append(contact_name)
|
||||
|
||||
return contacts
|
||||
|
||||
def add_contact_links_to_communication(communication, contact_name):
|
||||
contact_links = frappe.get_list("Dynamic Link", filters={
|
||||
"parenttype": "Contact",
|
||||
"parent": contact_name
|
||||
}, fields=["link_doctype", "link_name"])
|
||||
|
||||
if contact_links:
|
||||
for contact_link in contact_links:
|
||||
communication.add_link(contact_link.link_doctype, contact_link.link_name)
|
||||
|
||||
def parse_email(communication, email_strings):
|
||||
"""
|
||||
Parse email to add timeline links.
|
||||
When automatic email linking is enabled, an email from email_strings can contain
|
||||
a doctype and docname ie in the format `admin+doctype+docname@example.com`,
|
||||
the email is parsed and doctype and docname is extracted and timeline link is added.
|
||||
"""
|
||||
delimiter = "+"
|
||||
|
||||
for email_string in email_strings:
|
||||
if email_string:
|
||||
for email in email_string.split(","):
|
||||
if delimiter in email:
|
||||
email = email.split("@")[0]
|
||||
|
||||
doctype = unquote(email.split(delimiter)[1])
|
||||
docname = unquote(email.split(delimiter)[2])
|
||||
|
||||
if doctype and docname and frappe.db.exists(doctype, docname):
|
||||
communication.add_link(doctype, docname)
|
||||
|
||||
def get_email_without_link(email):
|
||||
"""
|
||||
returns email address without doctype links
|
||||
returns admin@example.com for email admin+doctype+docname@example.com
|
||||
"""
|
||||
email_id = email.split("@")[0].split("+")[0]
|
||||
email_host = email.split("@")[1]
|
||||
|
||||
return "{0}@{1}".format(email_id, email_host)
|
||||
|
|
|
|||
|
|
@ -71,12 +71,9 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
|
|||
"message_id":get_message_id().strip(" <>"),
|
||||
"read_receipt":read_receipt,
|
||||
"has_attachment": 1 if attachments else 0
|
||||
})
|
||||
comm.insert(ignore_permissions=True)
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
if not doctype:
|
||||
# if no reference given, then send it against the communication
|
||||
comm.db_set(dict(reference_doctype='Communication', reference_name=comm.name))
|
||||
comm.save(ignore_permissions=True)
|
||||
|
||||
if isinstance(attachments, string_types):
|
||||
attachments = json.loads(attachments)
|
||||
|
|
@ -557,5 +554,4 @@ def mark_email_as_seen(name=None):
|
|||
|
||||
frappe.response["type"] = 'binary'
|
||||
frappe.response["filename"] = "imaginary_pixel.png"
|
||||
frappe.response["filecontent"] = buffered_obj.getvalue()
|
||||
|
||||
frappe.response["filecontent"] = buffered_obj.getvalue()
|
||||
|
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
|||
|
||||
import frappe
|
||||
import unittest
|
||||
from six.moves.urllib.parse import quote
|
||||
test_records = frappe.get_test_records('Communication')
|
||||
|
||||
|
||||
|
|
@ -44,28 +45,153 @@ class TestCommunication(unittest.TestCase):
|
|||
self.assertFalse(frappe.utils.parse_addr(x)[0])
|
||||
|
||||
def test_circular_linking(self):
|
||||
content = "This was created to test circular linking"
|
||||
a = frappe.get_doc({
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"content": content,
|
||||
}).insert()
|
||||
"content": "This was created to test circular linking: Communication A",
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
b = frappe.get_doc({
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"content": content,
|
||||
"content": "This was created to test circular linking: Communication B",
|
||||
"reference_doctype": "Communication",
|
||||
"reference_name": a.name
|
||||
}).insert()
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
c = frappe.get_doc({
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"content": content,
|
||||
"content": "This was created to test circular linking: Communication C",
|
||||
"reference_doctype": "Communication",
|
||||
"reference_name": b.name
|
||||
}).insert()
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
a = frappe.get_doc("Communication", a.name)
|
||||
a.reference_doctype = "Communication"
|
||||
a.reference_name = c.name
|
||||
|
||||
self.assertRaises(frappe.CircularLinkingError, a.save)
|
||||
|
||||
def test_deduplication_timeline_links(self):
|
||||
frappe.delete_doc_if_exists("Note", "deduplication timeline links")
|
||||
|
||||
note = frappe.get_doc({
|
||||
"doctype": "Note",
|
||||
"title": "deduplication timeline links",
|
||||
"content": "deduplication timeline links"
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
comm = frappe.get_doc({
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"content": "Deduplication of Links",
|
||||
"communication_medium": "Email"
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
#adding same link twice
|
||||
comm.add_link(link_doctype="Note", link_name=note.name, autosave=True)
|
||||
comm.add_link(link_doctype="Note", link_name=note.name, autosave=True)
|
||||
|
||||
comm = frappe.get_doc("Communication", comm.name)
|
||||
|
||||
self.assertNotEqual(2, len(comm.timeline_links))
|
||||
|
||||
def test_contacts_attached(self):
|
||||
contact_sender = frappe.get_doc({
|
||||
"doctype": "Contact",
|
||||
"first_name": frappe.generate_hash(length=10),
|
||||
"email_id": "comm_sender@example.com"
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
contact_recipient = frappe.get_doc({
|
||||
"doctype": "Contact",
|
||||
"first_name": frappe.generate_hash(length=10),
|
||||
"email_id": "comm_recipient@example.com"
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
contact_cc = frappe.get_doc({
|
||||
"doctype": "Contact",
|
||||
"first_name": frappe.generate_hash(length=10),
|
||||
"email_id": "comm_cc@example.com"
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
comm = frappe.get_doc({
|
||||
"doctype": "Communication",
|
||||
"communication_medium": "Email",
|
||||
"subject": "Contacts Attached Test",
|
||||
"sender": "comm_sender@example.com",
|
||||
"recipients": "comm_recipient@example.com",
|
||||
"cc": "comm_cc@example.com"
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
comm = frappe.get_doc("Communication", comm.name)
|
||||
|
||||
contact_links = []
|
||||
for timeline_link in comm.timeline_links:
|
||||
contact_links.append(timeline_link.link_name)
|
||||
|
||||
self.assertIn(contact_sender.name, contact_links)
|
||||
self.assertIn(contact_recipient.name, contact_links)
|
||||
self.assertIn(contact_cc.name, contact_links)
|
||||
|
||||
def test_get_communication_data(self):
|
||||
from frappe.desk.form.load import get_communication_data
|
||||
|
||||
frappe.delete_doc_if_exists("Note", "get communication data")
|
||||
|
||||
note = frappe.get_doc({
|
||||
"doctype": "Note",
|
||||
"title": "get communication data",
|
||||
"content": "get communication data"
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
comm_note_1 = frappe.get_doc({
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"content": "Test Get Communication Data 1",
|
||||
"communication_medium": "Email"
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
comm_note_1.add_link(link_doctype="Note", link_name=note.name, autosave=True)
|
||||
|
||||
comm_note_2 = frappe.get_doc({
|
||||
"doctype": "Communication",
|
||||
"communication_type": "Communication",
|
||||
"content": "Test Get Communication Data 2",
|
||||
"communication_medium": "Email"
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
comm_note_2.add_link(link_doctype="Note", link_name=note.name, autosave=True)
|
||||
|
||||
comms = get_communication_data("Note", note.name, as_dict=True)
|
||||
|
||||
data = []
|
||||
for comm in comms:
|
||||
data.append(comm.name)
|
||||
|
||||
self.assertIn(comm_note_1.name, data)
|
||||
self.assertIn(comm_note_2.name, data)
|
||||
|
||||
def test_link_in_email(self):
|
||||
frappe.delete_doc_if_exists("Note", "test document link in email")
|
||||
|
||||
note = frappe.get_doc({
|
||||
"doctype": "Note",
|
||||
"title": "test document link in email",
|
||||
"content": "test document link in email"
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
comm = frappe.get_doc({
|
||||
"doctype": "Communication",
|
||||
"communication_medium": "Email",
|
||||
"subject": "Document Link in Email",
|
||||
"sender": "comm_sender@example.com",
|
||||
"recipients": "comm_recipient+{0}+{1}@example.com".format(quote("Note"), quote(note.name)),
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
doc_links = []
|
||||
for timeline_link in comm.timeline_links:
|
||||
doc_links.append((timeline_link.link_doctype, timeline_link.link_name))
|
||||
|
||||
self.assertIn(("Note", note.name), doc_links)
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"creation": "2019-05-21 09:47:23.043960",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"link_doctype",
|
||||
"link_name",
|
||||
"link_title"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "link_doctype",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Link DocType",
|
||||
"options": "DocType",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "link_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Link Name",
|
||||
"options": "link_doctype",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "link_title",
|
||||
"fieldtype": "Read Only",
|
||||
"in_list_view": 1,
|
||||
"label": "Link Title",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-05-21 09:47:23.043960",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Communication Link",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
13
frappe/core/doctype/communication_link/communication_link.py
Normal file
13
frappe/core/doctype/communication_link/communication_link.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class CommunicationLink(Document):
|
||||
pass
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Communication Link", ["link_doctype", "link_name"])
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
"no_copy": 0,
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
|
|
@ -1710,7 +1710,7 @@
|
|||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-04-08 12:19:53.415372",
|
||||
"modified": "2019-05-28 12:19:53.415372",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocField",
|
||||
|
|
|
|||
|
|
@ -22,9 +22,15 @@ frappe.ui.form.on('DocType', {
|
|||
}
|
||||
|
||||
if (!frm.is_new() && !frm.doc.istable) {
|
||||
frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => {
|
||||
frappe.set_route('List', frm.doc.name, 'List');
|
||||
});
|
||||
if (frm.doc.issingle) {
|
||||
frm.add_custom_button(__('Go to {0}', [frm.doc.name]), () => {
|
||||
frappe.set_route('Form', frm.doc.name);
|
||||
});
|
||||
} else {
|
||||
frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => {
|
||||
frappe.set_route('List', frm.doc.name, 'List');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(!frappe.boot.developer_mode && !frm.doc.custom) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -13,6 +13,7 @@ from frappe.utils import now, cint
|
|||
from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields
|
||||
from frappe.model.document import Document
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
from frappe.desk.notifications import delete_notification_count_for
|
||||
from frappe.modules import make_boilerplate, get_doc_path
|
||||
from frappe.database.schema import validate_column_name, validate_column_length
|
||||
|
|
@ -23,6 +24,14 @@ import frappe.website.render
|
|||
import json
|
||||
|
||||
class InvalidFieldNameError(frappe.ValidationError): pass
|
||||
class UniqueFieldnameError(frappe.ValidationError): pass
|
||||
class IllegalMandatoryError(frappe.ValidationError): pass
|
||||
class DoctypeLinkError(frappe.ValidationError): pass
|
||||
class WrongOptionsDoctypeLinkError(frappe.ValidationError): pass
|
||||
class HiddenAndMandatoryWithoutDefaultError(frappe.ValidationError): pass
|
||||
class NonUniqueError(frappe.ValidationError): pass
|
||||
class CannotIndexedError(frappe.ValidationError): pass
|
||||
class CannotCreateStandardDoctypeError(frappe.ValidationError): pass
|
||||
|
||||
form_grid_templates = {
|
||||
"fields": "templates/form_grid/fields.html"
|
||||
|
|
@ -39,7 +48,8 @@ class DocType(Document):
|
|||
- Validate series
|
||||
- Check fieldnames (duplication etc)
|
||||
- Clear permission table for child tables
|
||||
- Add `amended_from` and `amended_by` if Amendable"""
|
||||
- Add `amended_from` and `amended_by` if Amendable
|
||||
- Add custom field `auto_repeat` if Repeatable"""
|
||||
|
||||
self.check_developer_mode()
|
||||
|
||||
|
|
@ -68,6 +78,7 @@ class DocType(Document):
|
|||
validate_permissions(self)
|
||||
|
||||
self.make_amendable()
|
||||
self.make_repeatable()
|
||||
self.validate_website()
|
||||
|
||||
if not self.is_new():
|
||||
|
|
@ -101,7 +112,7 @@ class DocType(Document):
|
|||
return
|
||||
|
||||
if not frappe.conf.get("developer_mode") and not self.custom:
|
||||
frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."))
|
||||
frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."), CannotCreateStandardDoctypeError)
|
||||
|
||||
def setup_fields_to_fetch(self):
|
||||
'''Setup query to update values for newly set fetch values'''
|
||||
|
|
@ -121,7 +132,7 @@ class DocType(Document):
|
|||
link_fieldname, source_fieldname = df.fetch_from.split('.', 1)
|
||||
link_df = new_meta.get_field(link_fieldname)
|
||||
|
||||
if frappe.conf.db_type == 'postgres':
|
||||
if frappe.db.db_type == 'postgres':
|
||||
update_query = '''
|
||||
UPDATE `tab{doctype}`
|
||||
SET `{fieldname}` = source.`{source_fieldname}`
|
||||
|
|
@ -195,6 +206,9 @@ class DocType(Document):
|
|||
d.fieldname = d.fieldname + '_column'
|
||||
else:
|
||||
d.fieldname = d.fieldtype.lower().replace(" ","_") + "_" + str(d.idx)
|
||||
else:
|
||||
if d.fieldname in restricted:
|
||||
frappe.throw(_("Fieldname {0} is restricted").format(d.fieldname), InvalidFieldNameError)
|
||||
|
||||
d.fieldname = re.sub('''['",./%@()<>{}]''', '', d.fieldname)
|
||||
|
||||
|
|
@ -373,7 +387,7 @@ class DocType(Document):
|
|||
os.path.join(new_path, fname.replace(frappe.scrub(old), frappe.scrub(new)))])
|
||||
|
||||
self.rename_inside_controller(new, old, new_path)
|
||||
frappe.msgprint('Renamed files and replaced code in controllers, please check!')
|
||||
frappe.msgprint(_('Renamed files and replaced code in controllers, please check!'))
|
||||
|
||||
def rename_inside_controller(self, new, old, new_path):
|
||||
for fname in ('{}.js', '{}.py', '{}_list.js', '{}_calendar.js', 'test_{}.py', 'test_{}.js'):
|
||||
|
|
@ -515,6 +529,14 @@ class DocType(Document):
|
|||
"no_copy": 1
|
||||
})
|
||||
|
||||
def make_repeatable(self):
|
||||
"""If allow_auto_repeat is set, add auto_repeat custom field."""
|
||||
if self.allow_auto_repeat:
|
||||
if not frappe.db.exists('Custom Field', {'fieldname': 'auto_repeat', 'dt': self.name}):
|
||||
insert_after = self.fields[len(self.fields) - 1].fieldname
|
||||
df = dict(fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', options='Auto Repeat', insert_after=insert_after, read_only=1, no_copy=1, print_hide=1)
|
||||
create_custom_field(self.name, df)
|
||||
|
||||
def get_max_idx(self):
|
||||
"""Returns the highest `idx`"""
|
||||
max_idx = frappe.db.sql("""select max(idx) from `tabDocField` where parent = %s""",
|
||||
|
|
@ -542,7 +564,6 @@ def validate_fields_for_doctype(doctype):
|
|||
# this is separate because it is also called via custom field
|
||||
def validate_fields(meta):
|
||||
"""Validate doctype fields. Checks
|
||||
|
||||
1. There are no illegal characters in fieldnames
|
||||
2. If fieldnames are unique.
|
||||
3. Validate column length.
|
||||
|
|
@ -562,38 +583,38 @@ def validate_fields(meta):
|
|||
def check_illegal_characters(fieldname):
|
||||
validate_column_name(fieldname)
|
||||
|
||||
def check_unique_fieldname(fieldname):
|
||||
def check_unique_fieldname(docname, fieldname):
|
||||
duplicates = list(filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields)))
|
||||
if len(duplicates) > 1:
|
||||
frappe.throw(_("Fieldname {0} appears multiple times in rows {1}").format(fieldname, ", ".join(duplicates)))
|
||||
frappe.throw(_("{0}: Fieldname {1} appears multiple times in rows {2}").format(docname, fieldname, ", ".join(duplicates)), UniqueFieldnameError)
|
||||
|
||||
def check_fieldname_length(fieldname):
|
||||
validate_column_length(fieldname)
|
||||
|
||||
def check_illegal_mandatory(d):
|
||||
def check_illegal_mandatory(docname, d):
|
||||
if (d.fieldtype in no_value_fields) and d.fieldtype not in table_fields and d.reqd:
|
||||
frappe.throw(_("Field {0} of type {1} cannot be mandatory").format(d.label, d.fieldtype))
|
||||
frappe.throw(_("{0}: Field {1} of type {2} cannot be mandatory").format(docname, d.label, d.fieldtype), IllegalMandatoryError)
|
||||
|
||||
def check_link_table_options(d):
|
||||
def check_link_table_options(docname, d):
|
||||
if d.fieldtype in ("Link",) + table_fields:
|
||||
if not d.options:
|
||||
frappe.throw(_("Options required for Link or Table type field {0} in row {1}").format(d.label, d.idx))
|
||||
frappe.throw(_("{0}: Options required for Link or Table type field {1} in row {2}").format(docname, d.label, d.idx), DoctypeLinkError)
|
||||
if d.options=="[Select]" or d.options==d.parent:
|
||||
return
|
||||
if d.options != d.parent:
|
||||
options = frappe.db.get_value("DocType", d.options, "name")
|
||||
if not options:
|
||||
frappe.throw(_("Options must be a valid DocType for field {0} in row {1}").format(d.label, d.idx))
|
||||
frappe.throw(_("{0}: Options must be a valid DocType for field {1} in row {2}").format(docname, d.label, d.idx), WrongOptionsDoctypeLinkError)
|
||||
elif not (options == d.options):
|
||||
frappe.throw(_("Options {0} must be the same as doctype name {1} for the field {2}")
|
||||
.format(d.options, options, d.label))
|
||||
frappe.throw(_("{0}: Options {1} must be the same as doctype name {2} for the field {3}", DoctypeLinkError)
|
||||
.format(docname, d.options, options, d.label))
|
||||
else:
|
||||
# fix case
|
||||
d.options = options
|
||||
|
||||
def check_hidden_and_mandatory(d):
|
||||
def check_hidden_and_mandatory(docname, d):
|
||||
if d.hidden and d.reqd and not d.default:
|
||||
frappe.throw(_("Field {0} in row {1} cannot be hidden and mandatory without default").format(d.label, d.idx))
|
||||
frappe.throw(_("{0}: Field {1} in row {2} cannot be hidden and mandatory without default").format(docname, d.label, d.idx), HiddenAndMandatoryWithoutDefaultError)
|
||||
|
||||
def check_width(d):
|
||||
if d.fieldtype == "Currency" and cint(d.width) < 100:
|
||||
|
|
@ -616,7 +637,9 @@ def validate_fields(meta):
|
|||
frappe.throw(_("Options 'Dynamic Link' type of field must point to another Link Field with options as 'DocType'"))
|
||||
|
||||
def check_illegal_default(d):
|
||||
if d.fieldtype == "Check" and d.default and d.default not in ('0', '1'):
|
||||
if d.fieldtype == "Check" and not d.default:
|
||||
d.default = '0'
|
||||
if d.fieldtype == "Check" and d.default not in ('0', '1'):
|
||||
frappe.throw(_("Default for 'Check' type of field must be either '0' or '1'"))
|
||||
if d.fieldtype == "Select" and d.default and (d.default not in d.options.split("\n")):
|
||||
frappe.throw(_("Default for {0} must be an option").format(d.fieldname))
|
||||
|
|
@ -625,14 +648,14 @@ def validate_fields(meta):
|
|||
if d.fieldtype in ("Currency", "Float", "Percent") and d.precision is not None and not (1 <= cint(d.precision) <= 6):
|
||||
frappe.throw(_("Precision should be between 1 and 6"))
|
||||
|
||||
def check_unique_and_text(d):
|
||||
def check_unique_and_text(docname, d):
|
||||
if meta.issingle:
|
||||
d.unique = 0
|
||||
d.search_index = 0
|
||||
|
||||
if getattr(d, "unique", False):
|
||||
if d.fieldtype not in ("Data", "Link", "Read Only"):
|
||||
frappe.throw(_("Fieldtype {0} for {1} cannot be unique").format(d.fieldtype, d.label))
|
||||
frappe.throw(_("{0}: Fieldtype {1} for {2} cannot be unique").format(docname, d.fieldtype, d.label), NonUniqueError)
|
||||
|
||||
if not d.get("__islocal") and frappe.db.has_column(d.parent, d.fieldname):
|
||||
has_non_unique_values = frappe.db.sql("""select `{fieldname}`, count(*)
|
||||
|
|
@ -641,10 +664,10 @@ def validate_fields(meta):
|
|||
doctype=d.parent, fieldname=d.fieldname))
|
||||
|
||||
if has_non_unique_values and has_non_unique_values[0][0]:
|
||||
frappe.throw(_("Field '{0}' cannot be set as Unique as it has non-unique values").format(d.label))
|
||||
frappe.throw(_("{0}: Field '{1}' cannot be set as Unique as it has non-unique values").format(docname, d.label), NonUniqueError)
|
||||
|
||||
if d.search_index and d.fieldtype in ("Text", "Long Text", "Small Text", "Code", "Text Editor"):
|
||||
frappe.throw(_("Fieldtype {0} for {1} cannot be indexed").format(d.fieldtype, d.label))
|
||||
frappe.throw(_("{0}:Fieldtype {1} for {2} cannot be indexed").format(docname, d.fieldtype, d.label), CannotIndexedError)
|
||||
|
||||
def check_fold(fields):
|
||||
fold_exists = False
|
||||
|
|
@ -790,21 +813,20 @@ def validate_fields(meta):
|
|||
for d in fields:
|
||||
if not d.permlevel: d.permlevel = 0
|
||||
if d.fieldtype not in table_fields: d.allow_bulk_edit = 0
|
||||
if d.fieldtype == "Barcode": d.ignore_xss_filter = 1
|
||||
if not d.fieldname:
|
||||
d.fieldname = d.fieldname.lower()
|
||||
|
||||
check_illegal_characters(d.fieldname)
|
||||
check_unique_fieldname(d.fieldname)
|
||||
check_unique_fieldname(meta.get("name"), d.fieldname)
|
||||
check_fieldname_length(d.fieldname)
|
||||
check_illegal_mandatory(d)
|
||||
check_link_table_options(d)
|
||||
check_illegal_mandatory(meta.get("name"), d)
|
||||
check_link_table_options(meta.get("name"), d)
|
||||
check_dynamic_link_options(d)
|
||||
check_hidden_and_mandatory(d)
|
||||
check_hidden_and_mandatory(meta.get("name"), d)
|
||||
check_in_list_view(d)
|
||||
check_in_global_search(d)
|
||||
check_illegal_default(d)
|
||||
check_unique_and_text(d)
|
||||
check_unique_and_text(meta.get("name"), d)
|
||||
check_illegal_depends_on_conditions(d)
|
||||
check_table_multiselect_option(d)
|
||||
scrub_options_in_select(d)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ from __future__ import unicode_literals
|
|||
|
||||
import frappe
|
||||
import unittest
|
||||
from frappe.core.doctype.doctype.doctype import UniqueFieldnameError, IllegalMandatoryError, DoctypeLinkError, WrongOptionsDoctypeLinkError,\
|
||||
HiddenAndMandatoryWithoutDefaultError, CannotIndexedError, InvalidFieldNameError, CannotCreateStandardDoctypeError
|
||||
|
||||
# test_records = frappe.get_test_records('DocType')
|
||||
|
||||
|
|
@ -226,3 +228,65 @@ class TestDocType(unittest.TestCase):
|
|||
raise
|
||||
finally:
|
||||
frappe.flags.allow_doctype_export = 0
|
||||
|
||||
def test_unique_field_name_for_two_fields(self):
|
||||
doc = self.new_doctype('Test Unique Field')
|
||||
field_1 = doc.append('fields', {})
|
||||
field_1.fieldname = 'some_fieldname_1'
|
||||
field_1.fieldtype = 'Data'
|
||||
|
||||
field_2 = doc.append('fields', {})
|
||||
field_2.fieldname = 'some_fieldname_1'
|
||||
field_2.fieldtype = 'Data'
|
||||
|
||||
self.assertRaises(UniqueFieldnameError, doc.insert)
|
||||
|
||||
def test_fieldname_is_not_name(self):
|
||||
doc = self.new_doctype('Test Name Field')
|
||||
field_1 = doc.append('fields', {})
|
||||
field_1.label = 'Name'
|
||||
field_1.fieldtype = 'Data'
|
||||
doc.insert()
|
||||
self.assertEqual(doc.fields[1].fieldname, "name1")
|
||||
doc.fields[1].fieldname = 'name'
|
||||
self.assertRaises(InvalidFieldNameError, doc.save)
|
||||
|
||||
def test_illegal_mandatory_validation(self):
|
||||
doc = self.new_doctype('Test Illegal mandatory')
|
||||
field_1 = doc.append('fields', {})
|
||||
field_1.fieldname = 'some_fieldname_1'
|
||||
field_1.fieldtype = 'Section Break'
|
||||
field_1.reqd = 1
|
||||
|
||||
self.assertRaises(IllegalMandatoryError, doc.insert)
|
||||
|
||||
def test_link_with_wrong_and_no_options(self):
|
||||
doc = self.new_doctype('Test link')
|
||||
field_1 = doc.append('fields', {})
|
||||
field_1.fieldname = 'some_fieldname_1'
|
||||
field_1.fieldtype = 'Link'
|
||||
|
||||
self.assertRaises(DoctypeLinkError, doc.insert)
|
||||
|
||||
field_1.options = 'wrongdoctype'
|
||||
|
||||
self.assertRaises(WrongOptionsDoctypeLinkError, doc.insert)
|
||||
|
||||
def test_hidden_and_mandatory_without_default(self):
|
||||
doc = self.new_doctype('Test hidden and mandatory')
|
||||
field_1 = doc.append('fields', {})
|
||||
field_1.fieldname = 'some_fieldname_1'
|
||||
field_1.fieldtype = 'Data'
|
||||
field_1.reqd = 1
|
||||
field_1.hidden = 1
|
||||
|
||||
self.assertRaises(HiddenAndMandatoryWithoutDefaultError, doc.insert)
|
||||
|
||||
def test_field_can_not_be_indexed_validation(self):
|
||||
doc = self.new_doctype('Test index')
|
||||
field_1 = doc.append('fields', {})
|
||||
field_1.fieldname = 'some_fieldname_1'
|
||||
field_1.fieldtype = 'Long Text'
|
||||
field_1.search_index = 1
|
||||
|
||||
self.assertRaises(CannotIndexedError, doc.insert)
|
||||
|
|
|
|||
|
|
@ -1,95 +1,54 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "field:domain",
|
||||
"beta": 0,
|
||||
"creation": "2017-05-03 15:07:39.752820",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"autoname": "field:domain",
|
||||
"creation": "2017-05-03 15:07:39.752820",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"domain"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "domain",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Domain",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "domain",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Domain",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-09-15 12:26:21.827149",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Domain",
|
||||
"name_case": "",
|
||||
"owner": "makarand@erpnext.com",
|
||||
],
|
||||
"modified": "2019-06-30 13:24:13.732202",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Domain",
|
||||
"owner": "makarand@erpnext.com",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 0,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"search_fields": "domain",
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "domain",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"search_fields": "domain",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "domain"
|
||||
}
|
||||
|
|
@ -1,125 +1,47 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2017-01-13 04:55:18.835023",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"creation": "2017-01-13 04:55:18.835023",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"link_doctype",
|
||||
"link_name",
|
||||
"link_title"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "link_doctype",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Link DocType",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "DocType",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "link_doctype",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Link DocType",
|
||||
"options": "DocType",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "link_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Link Name",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "link_doctype",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "link_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Link Name",
|
||||
"options": "link_doctype",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "link_title",
|
||||
"fieldtype": "Read Only",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Link Title",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "link_title",
|
||||
"fieldtype": "Read Only",
|
||||
"in_list_view": 1,
|
||||
"label": "Link Title",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-01-17 14:25:49.140730",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Dynamic Link",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-05-16 19:54:31.400026",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Dynamic Link",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
"""
|
||||
record of files
|
||||
|
||||
|
|
@ -446,7 +447,7 @@ class File(NestedSet):
|
|||
def validate_url(self, df=None):
|
||||
if self.file_url:
|
||||
if not self.file_url.startswith(("http://", "https://", "/files/", "/private/files/")):
|
||||
frappe.throw("URL must start with 'http://' or 'https://'")
|
||||
frappe.throw(_("URL must start with 'http://' or 'https://'"))
|
||||
return
|
||||
|
||||
self.file_url = unquote(self.file_url)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -9,16 +9,22 @@ frappe.ui.form.on('Role Permission for Page and Report', {
|
|||
refresh: function(frm) {
|
||||
frm.disable_save();
|
||||
frm.role_area.hide();
|
||||
|
||||
frm.add_custom_button(__("Reset to defaults"), function() {
|
||||
frm.trigger("reset_roles");
|
||||
});
|
||||
|
||||
frm.add_custom_button(__("Update"), function() {
|
||||
frm.trigger("update_report_page_data");
|
||||
}).addClass('btn-primary');
|
||||
frm.events.add_custom_buttons(frm);
|
||||
},
|
||||
|
||||
|
||||
add_custom_buttons: function(frm) {
|
||||
frm.clear_custom_buttons();
|
||||
if(frm.doc.set_role_for && frm.doc[frappe.model.scrub(frm.doc.set_role_for)]) {
|
||||
frm.add_custom_button(__("Reset to defaults"), function() {
|
||||
frm.trigger("reset_roles");
|
||||
});
|
||||
|
||||
frm.add_custom_button(__("Update"), function() {
|
||||
frm.trigger("update_report_page_data");
|
||||
}).addClass('btn-primary');
|
||||
}
|
||||
},
|
||||
|
||||
onload: function(frm) {
|
||||
if(!frm.roles_editor) {
|
||||
frm.role_area = $('<div style="min-height: 300px">')
|
||||
|
|
@ -48,14 +54,20 @@ frappe.ui.form.on('Role Permission for Page and Report', {
|
|||
},
|
||||
|
||||
page: function(frm) {
|
||||
frm.events.add_custom_buttons(frm);
|
||||
if(frm.doc.page) {
|
||||
frm.trigger("set_report_page_data");
|
||||
} else {
|
||||
frm.trigger("set_role_for");
|
||||
}
|
||||
},
|
||||
|
||||
report: function(frm){
|
||||
frm.events.add_custom_buttons(frm);
|
||||
if(frm.doc.report) {
|
||||
frm.trigger("set_report_page_data");
|
||||
} else {
|
||||
frm.trigger("set_role_for");
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -107,7 +119,7 @@ frappe.ui.form.on('Role Permission for Page and Report', {
|
|||
if(!frm.doc.set_role_for){
|
||||
frappe.throw(__("Mandatory field: set role for"))
|
||||
}
|
||||
|
||||
|
||||
if(frm.doc.set_role_for && !frm.doc[frm.doc.set_role_for.toLocaleLowerCase()]) {
|
||||
frappe.throw(__("Mandatory field: {0}", [frm.doc.set_role_for]))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ frappe.ui.form.on('Success Action', {
|
|||
validate: (frm) => {
|
||||
const checked_actions = frm.action_multicheck.get_checked_options();
|
||||
if (checked_actions.length < 2) {
|
||||
frappe.msgprint('Select atleast 2 actions');
|
||||
frappe.msgprint(__('Select atleast 2 actions'));
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
|
|
@ -18,6 +19,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "localization",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
|
|
@ -49,6 +51,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
|
|
@ -82,6 +85,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
|
|
@ -114,6 +118,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
|
|
@ -145,6 +150,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "time_zone",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
|
|
@ -176,6 +182,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "is_first_startup",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
|
|
@ -208,6 +215,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "setup_complete",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
|
|
@ -240,6 +248,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 1,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "date_and_number_format",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
|
|
@ -271,6 +280,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "date_format",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
|
|
@ -303,6 +313,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "column_break_7",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
|
|
@ -334,6 +345,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "number_format",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
|
|
@ -366,6 +378,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "float_precision",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
|
|
@ -399,6 +412,7 @@
|
|||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "If not set, the currency precision will depend on number format",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "currency_precision",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
|
|
@ -432,6 +446,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 1,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "sec_backup_limit",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
|
|
@ -466,6 +481,7 @@
|
|||
"columns": 0,
|
||||
"default": "3",
|
||||
"description": "Older backups will be automatically deleted",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "backup_limit",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 0,
|
||||
|
|
@ -498,6 +514,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 1,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "background_workers",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
|
|
@ -531,6 +548,7 @@
|
|||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "Run scheduled jobs only if checked",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "enable_scheduler",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
|
|
@ -562,6 +580,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "scheduler_last_event",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
|
|
@ -594,6 +613,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 1,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "permissions",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
|
|
@ -628,6 +648,7 @@
|
|||
"columns": 0,
|
||||
"default": "0",
|
||||
"description": "If Apply Strict User Permission is checked and User Permission is defined for a DocType for a User, then all the documents where value of the link is blank, will not be shown to that User",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "apply_strict_user_permissions",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
|
|
@ -660,6 +681,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 1,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "security",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
|
|
@ -693,6 +715,7 @@
|
|||
"columns": 0,
|
||||
"default": "06:00",
|
||||
"description": "Session Expiry in Hours e.g. 06:00",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "session_expiry",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
|
|
@ -727,6 +750,7 @@
|
|||
"columns": 0,
|
||||
"default": "720:00",
|
||||
"description": "In Hours",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "session_expiry_mobile",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
|
|
@ -759,75 +783,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"description": "If enabled, the password strength will be enforced based on the Minimum Password Score value. A value of 2 being medium strong and 4 being very strong.",
|
||||
"fieldname": "enable_password_policy",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Enable Password Policy",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "2",
|
||||
"depends_on": "eval:doc.enable_password_policy==1",
|
||||
"fieldname": "minimum_password_score",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Minimum Password Score",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "2\n3\n4",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
|
|
@ -860,6 +816,7 @@
|
|||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "Note: Multiple sessions will be allowed in case of mobile device",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "deny_multiple_sessions",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
|
|
@ -894,6 +851,7 @@
|
|||
"columns": 0,
|
||||
"default": "0",
|
||||
"description": "User can login using Email id or Mobile number",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "allow_login_using_mobile_number",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
|
|
@ -928,6 +886,7 @@
|
|||
"columns": 0,
|
||||
"default": "0",
|
||||
"description": "User can login using Email id or User Name",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "allow_login_using_user_name",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
|
|
@ -962,6 +921,7 @@
|
|||
"columns": 0,
|
||||
"default": "1",
|
||||
"description": "",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "allow_error_traceback",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
|
|
@ -994,6 +954,177 @@
|
|||
"bold": 0,
|
||||
"collapsible": 1,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "password_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Password",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "In Days",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "force_user_to_reset_password",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Force User to Reset Password",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "column_break_31",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"description": "If enabled, the password strength will be enforced based on the Minimum Password Score value. A value of 2 being medium strong and 4 being very strong.",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "enable_password_policy",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Enable Password Policy",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "2",
|
||||
"depends_on": "eval:doc.enable_password_policy==1",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "minimum_password_score",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Minimum Password Score",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "2\n3\n4",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 1,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "brute_force_security",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
|
|
@ -1026,6 +1157,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "allow_consecutive_login_attempts",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 0,
|
||||
|
|
@ -1058,6 +1190,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "column_break_34",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
|
|
@ -1091,6 +1224,7 @@
|
|||
"columns": 0,
|
||||
"default": "60",
|
||||
"description": "In seconds",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "allow_login_after_fail",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 0,
|
||||
|
|
@ -1123,6 +1257,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 1,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "two_factor_authentication",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
|
|
@ -1155,6 +1290,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "enable_two_factor_auth",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
|
|
@ -1190,6 +1326,7 @@
|
|||
"default": "0",
|
||||
"depends_on": "enable_two_factor_auth",
|
||||
"description": "If enabled, users who login from Restricted IP Address, won't be prompted for Two Factor Auth",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "bypass_2fa_for_retricted_ip_users",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
|
|
@ -1224,6 +1361,7 @@
|
|||
"columns": 0,
|
||||
"depends_on": "enable_two_factor_auth",
|
||||
"description": "If enabled, all users can login from any IP Address using Two Factor Auth. This can also be set only for specific user(s) in User Page",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "bypass_restrict_ip_check_if_2fa_enabled",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
|
|
@ -1259,6 +1397,7 @@
|
|||
"default": "OTP App",
|
||||
"depends_on": "",
|
||||
"description": "Choose authentication method to be used by all users",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "two_factor_method",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
|
|
@ -1294,6 +1433,7 @@
|
|||
"columns": 0,
|
||||
"depends_on": "eval:doc.two_factor_method == \"OTP App\"",
|
||||
"description": "Time in seconds to retain QR code image on server. Min:<strong>240</strong>",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "lifespan_qrcode_image",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 0,
|
||||
|
|
@ -1328,6 +1468,7 @@
|
|||
"columns": 0,
|
||||
"default": "Frappe Framework",
|
||||
"depends_on": "enable_two_factor_auth",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "otp_issuer_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
|
|
@ -1361,6 +1502,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 1,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "email",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
|
|
@ -1394,6 +1536,7 @@
|
|||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "Your organization name and address for the email footer.",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "email_footer_address",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 0,
|
||||
|
|
@ -1426,6 +1569,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
|
|
@ -1457,6 +1601,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "disable_standard_email_footer",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
|
|
@ -1489,6 +1634,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "hide_footer_in_auto_email_reports",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
|
|
@ -1521,6 +1667,7 @@
|
|||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "chat",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
|
|
@ -1554,6 +1701,7 @@
|
|||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "enable_chat",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
|
|
@ -1587,6 +1735,7 @@
|
|||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "use_socketio_to_upload_file",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
|
|
@ -1624,7 +1773,7 @@
|
|||
"issingle": 1,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-01-30 11:02:41.011412",
|
||||
"modified": "2019-04-16 13:26:09.247487",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from frappe import _
|
|||
from frappe.model.document import Document
|
||||
from frappe.model import no_value_fields
|
||||
from frappe.translate import set_default_language
|
||||
from frappe.utils import cint
|
||||
from frappe.utils import cint, today
|
||||
from frappe.utils.momentjs import get_all_timezones
|
||||
from frappe.twofactor import toggle_two_factor_auth
|
||||
|
||||
|
|
@ -35,6 +35,11 @@ class SystemSettings(Document):
|
|||
self.bypass_2fa_for_retricted_ip_users = 0
|
||||
self.bypass_restrict_ip_check_if_2fa_enabled = 0
|
||||
|
||||
frappe.flags.update_last_reset_password_date = False
|
||||
if (self.force_user_to_reset_password and
|
||||
not cint(frappe.db.get_single_value("System Settings", "force_user_to_reset_password"))):
|
||||
frappe.flags.update_last_reset_password_date = True
|
||||
|
||||
def on_update(self):
|
||||
for df in self.meta.get("fields"):
|
||||
if df.fieldtype not in no_value_fields:
|
||||
|
|
@ -47,6 +52,16 @@ class SystemSettings(Document):
|
|||
frappe.cache().delete_value('time_zone')
|
||||
frappe.local.system_settings = {}
|
||||
|
||||
if frappe.flags.update_last_reset_password_date:
|
||||
update_last_reset_password_date()
|
||||
|
||||
def update_last_reset_password_date():
|
||||
frappe.db.sql(""" UPDATE `tabUser`
|
||||
SET
|
||||
last_password_reset_date = %s
|
||||
WHERE
|
||||
last_password_reset_date is null or last_password_reset_date = ''""", today())
|
||||
|
||||
@frappe.whitelist()
|
||||
def load():
|
||||
if not "System Manager" in frappe.get_roles():
|
||||
|
|
|
|||
|
|
@ -3,5 +3,19 @@
|
|||
|
||||
|
||||
frappe.ui.form.on('Translation', {
|
||||
refresh: function(frm) {
|
||||
if(frm.is_new() || !(["Saved", "Deleted"].includes(frm.doc.status))) return;
|
||||
frm.add_custom_button('Contribute', function() {
|
||||
frappe.call({
|
||||
method: 'frappe.core.doctype.translation.translation.contribute_translation',
|
||||
args: {
|
||||
"language": frm.doc.language,
|
||||
"contributor": frm.doc.owner,
|
||||
"source_name": frm.doc.source_name,
|
||||
"target_name": frm.doc.target_name,
|
||||
"doc_name": frm.doc.name
|
||||
}
|
||||
});
|
||||
}).addClass('btn-primary');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,203 +1,91 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 1,
|
||||
"allow_rename": 0,
|
||||
"autoname": "hash",
|
||||
"beta": 0,
|
||||
"creation": "2016-02-17 12:21:16.175465",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 0,
|
||||
"engine": "InnoDB",
|
||||
"allow_import": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2016-02-17 12:21:16.175465",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"language",
|
||||
"section_break_4",
|
||||
"source_name",
|
||||
"column_break_6",
|
||||
"target_name",
|
||||
"section_break_6",
|
||||
"status",
|
||||
"contributed_translation_doctype_name"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Language",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Language",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 1,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Language",
|
||||
"options": "Language",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "If your data is in HTML, please copy paste the exact HTML code with the tags.",
|
||||
"fieldname": "source_name",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Source Text",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"description": "If your data is in HTML, please copy paste the exact HTML code with the tags.",
|
||||
"fieldname": "source_name",
|
||||
"fieldtype": "Code",
|
||||
"label": "Source Text"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "target_name",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Translated Text",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "target_name",
|
||||
"fieldtype": "Code",
|
||||
"in_list_view": 1,
|
||||
"label": "Translated Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "Saved",
|
||||
"depends_on": "eval: !doc.__islocal",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "Saved\nContributed\nVerified\nPR sent\nDeleted",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "contributed_translation_doctype_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Contributed Translation Doctype Name",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2016-12-29 14:39:48.571006",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Translation",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"modified": "2019-06-18 19:03:38.640990",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Translation",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"is_custom": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "source_name",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "source_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ import frappe
|
|||
from frappe.model.document import Document
|
||||
from frappe.translate import clear_cache
|
||||
from frappe.utils import strip_html_tags, is_html
|
||||
from frappe.integrations.utils import make_post_request
|
||||
import json
|
||||
|
||||
class Translation(Document):
|
||||
def validate(self):
|
||||
|
|
@ -21,3 +23,39 @@ class Translation(Document):
|
|||
|
||||
def on_trash(self):
|
||||
clear_cache()
|
||||
|
||||
def onload(self):
|
||||
if self.contributed_translation_doctype_name:
|
||||
data = {"data": json.dumps({
|
||||
"doc_name": self.contributed_translation_doctype_name
|
||||
})}
|
||||
try:
|
||||
response = make_post_request(url=frappe.get_hooks("translation_contribution_status")[0], data=data)
|
||||
except Exception:
|
||||
frappe.msgprint("Something went wrong. Please check error log for more details")
|
||||
if response.get("message").get("message") == "Contributed Translation has been deleted":
|
||||
self.status = "Deleted"
|
||||
self.contributed_translation_doctype_name = ""
|
||||
self.save()
|
||||
else:
|
||||
self.status = response.get("message").get("status")
|
||||
self.save()
|
||||
|
||||
@frappe.whitelist()
|
||||
def contribute_translation(language, contributor, source_name, target_name, doc_name):
|
||||
data = {"data": json.dumps({
|
||||
"language": language,
|
||||
"contributor": contributor,
|
||||
"source_name": source_name,
|
||||
"target_name": target_name,
|
||||
"posting_date": frappe.utils.nowdate()
|
||||
})}
|
||||
try:
|
||||
response = make_post_request(url=frappe.get_hooks("translation_contribution_url")[0], data=data)
|
||||
except Exception:
|
||||
frappe.msgprint("Something went wrong while contributing translation. Please check error log for more details")
|
||||
if response.get("message").get("message") == "Already exists":
|
||||
frappe.msgprint("Translation already exists")
|
||||
elif response.get("message").get("message") == "Added to contribution list":
|
||||
frappe.set_value("Translation", doc_name, "contributed_translation_doctype_name", response.get("message").get("doc_name"))
|
||||
frappe.msgprint("Translation successfully contributed")
|
||||
|
|
|
|||
|
|
@ -268,21 +268,38 @@ class TestUser(unittest.TestCase):
|
|||
self.assertEqual(result['feedback']['password_policy_validation_passed'], True)
|
||||
|
||||
def test_comment_mentions(self):
|
||||
user_name = "@test.comment@example.com"
|
||||
self.assertEqual(extract_mentions(user_name)[0], "test.comment@example.com")
|
||||
user_name = "@test.comment@test-example.com"
|
||||
self.assertEqual(extract_mentions(user_name)[0], "test.comment@test-example.com")
|
||||
user_name = "Testing comment, @test-user please check."
|
||||
self.assertEqual(extract_mentions(user_name)[0], "test-user")
|
||||
user_name = "Testing comment, @test.user@example.com please check."
|
||||
self.assertEqual(extract_mentions(user_name)[0], "test.user@example.com")
|
||||
user_name = "<div>@test_user@example.com and @test.again@example1.com</div><div>This is a test.</div>"
|
||||
self.assertEqual(extract_mentions(user_name)[0], "test_user@example.com")
|
||||
self.assertEqual(extract_mentions(user_name)[1], "test.again@example1.com")
|
||||
user_name = "<div>@user@example.com</a> Test @test-comment@xyz.com</div><div>Test for comment mentions @test@abc.com</div>"
|
||||
self.assertEqual(extract_mentions(user_name)[0], "user@example.com")
|
||||
self.assertEqual(extract_mentions(user_name)[1], "test-comment@xyz.com")
|
||||
self.assertEqual(extract_mentions(user_name)[2], "test@abc.com")
|
||||
comment = '''
|
||||
<span class="mention" data-id="test.comment@example.com" data-value="Test" data-denotation-char="@">
|
||||
<span><span class="ql-mention-denotation-char">@</span>Test</span>
|
||||
</span>
|
||||
'''
|
||||
self.assertEqual(extract_mentions(comment)[0], "test.comment@example.com")
|
||||
|
||||
comment = '''
|
||||
<div>
|
||||
Testing comment,
|
||||
<span class="mention" data-id="test.comment@example.com" data-value="Test" data-denotation-char="@">
|
||||
<span><span class="ql-mention-denotation-char">@</span>Test</span>
|
||||
</span>
|
||||
please check
|
||||
</div>
|
||||
'''
|
||||
self.assertEqual(extract_mentions(comment)[0], "test.comment@example.com")
|
||||
comment = '''
|
||||
<div>
|
||||
Testing comment for
|
||||
<span class="mention" data-id="test_user@example.com" data-value="Test" data-denotation-char="@">
|
||||
<span><span class="ql-mention-denotation-char">@</span>Test</span>
|
||||
</span>
|
||||
and
|
||||
<span class="mention" data-id="test.again@example1.com" data-value="Test" data-denotation-char="@">
|
||||
<span><span class="ql-mention-denotation-char">@</span>Test</span>
|
||||
</span>
|
||||
please check
|
||||
</div>
|
||||
'''
|
||||
self.assertEqual(extract_mentions(comment)[0], "test_user@example.com")
|
||||
self.assertEqual(extract_mentions(comment)[1], "test.again@example1.com")
|
||||
|
||||
def delete_contact(user):
|
||||
frappe.db.sql("DELETE FROM `tabContact` WHERE `email_id`= %s", user)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -4,11 +4,12 @@
|
|||
from __future__ import unicode_literals, print_function
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, has_gravatar, format_datetime, now_datetime, get_formatted_email
|
||||
from frappe.utils import cint, has_gravatar, format_datetime, now_datetime, get_formatted_email, today
|
||||
from frappe import throw, msgprint, _
|
||||
from frappe.utils.password import update_password as _update_password
|
||||
from frappe.desk.notifications import clear_notifications
|
||||
from frappe.utils.user import get_system_managers
|
||||
from bs4 import BeautifulSoup
|
||||
import frappe.permissions
|
||||
import frappe.share
|
||||
import re
|
||||
|
|
@ -95,7 +96,7 @@ class User(Document):
|
|||
clear_notifications(user=self.name)
|
||||
frappe.clear_cache(user=self.name)
|
||||
self.send_password_notification(self.__new_password)
|
||||
create_contact(self)
|
||||
create_contact(self, ignore_mandatory=True)
|
||||
if self.name not in ('Administrator', 'Guest') and not self.user_image:
|
||||
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name)
|
||||
|
||||
|
|
@ -154,7 +155,7 @@ class User(Document):
|
|||
if new_password and not self.flags.in_insert:
|
||||
_update_password(user=self.name, pwd=new_password, logout_all_sessions=self.logout_all_sessions)
|
||||
|
||||
if self.send_password_update_notification:
|
||||
if self.send_password_update_notification and self.enabled:
|
||||
self.password_update_mail(new_password)
|
||||
frappe.msgprint(_("New password emailed"))
|
||||
|
||||
|
|
@ -218,13 +219,17 @@ class User(Document):
|
|||
def validate_reset_password(self):
|
||||
pass
|
||||
|
||||
def reset_password(self, send_email=False):
|
||||
def reset_password(self, send_email=False, password_expired=False):
|
||||
from frappe.utils import random_string, get_url
|
||||
|
||||
key = random_string(32)
|
||||
self.db_set("reset_password_key", key)
|
||||
link = get_url("/update-password?key=" + key)
|
||||
|
||||
url = "/update-password?key=" + key
|
||||
if password_expired:
|
||||
url = "/update-password?key=" + key + '&password_expired=true'
|
||||
|
||||
link = get_url(url)
|
||||
if send_email:
|
||||
self.password_reset_mail(link)
|
||||
|
||||
|
|
@ -591,6 +596,9 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password=
|
|||
|
||||
frappe.local.login_manager.login_as(user)
|
||||
|
||||
frappe.db.set_value("User", user,
|
||||
'last_password_reset_date', today())
|
||||
|
||||
if user_doc.user_type == "System User":
|
||||
return "/desk"
|
||||
else:
|
||||
|
|
@ -936,11 +944,13 @@ def notify_admin_access_to_system_manager(login_manager=None):
|
|||
)
|
||||
|
||||
def extract_mentions(txt):
|
||||
"""Find all instances of @name in the string.
|
||||
The mentions will be separated by non-word characters or may appear at the start of the string"""
|
||||
txt = txt.replace("<div>", "<div> ")
|
||||
txt = re.sub(r'(<[a-zA-Z\/][^>]*>)', '', txt)
|
||||
return re.findall(r'(?:[^\w\.\-\@]|^)@([\w\.\-\@]*)', txt)
|
||||
"""Find all instances of @mentions in the html."""
|
||||
soup = BeautifulSoup(txt, 'html.parser')
|
||||
emails = []
|
||||
for mention in soup.find_all(class_='mention'):
|
||||
email = mention['data-id']
|
||||
emails.append(email)
|
||||
return emails
|
||||
|
||||
def handle_password_test_fail(result):
|
||||
suggestions = result['feedback']['suggestions'][0] if result['feedback']['suggestions'] else ''
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import frappe
|
|||
import unittest
|
||||
|
||||
class TestUserPermission(unittest.TestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql("DELETE FROM `tabUser Permission` WHERE `user`='test_bulk_creation_update@example.com'")
|
||||
|
||||
def test_default_user_permission_validation(self):
|
||||
user = create_user('test_default_permission@example.com')
|
||||
param = get_params(user, 'User', user.name, is_default=1)
|
||||
|
|
@ -21,39 +24,69 @@ class TestUserPermission(unittest.TestCase):
|
|||
''' Create User permission for User having access to all applicable Doctypes'''
|
||||
user = create_user('test_bulk_creation_update@example.com')
|
||||
param = get_params(user, 'User', user.name)
|
||||
created = add_user_permissions(param)
|
||||
self.assertEquals(created, 1)
|
||||
is_created = add_user_permissions(param)
|
||||
self.assertEquals(is_created, 1)
|
||||
|
||||
def test_for_apply_to_all_on_update_from_apply_all(self):
|
||||
user = create_user('test_bulk_creation_update@example.com')
|
||||
param = get_params(user, 'User', user.name)
|
||||
|
||||
# Initially create User Permission document with apply_to_all checked
|
||||
is_created = add_user_permissions(param)
|
||||
|
||||
self.assertEquals(is_created, 1)
|
||||
is_created = add_user_permissions(param)
|
||||
|
||||
# User Permission should not be changed
|
||||
self.assertEquals(is_created, 0)
|
||||
|
||||
def test_for_applicable_on_update_from_apply_to_all(self):
|
||||
''' Update User Permission from all to some applicable Doctypes'''
|
||||
user = create_user('test_bulk_creation_update@example.com')
|
||||
param = get_params(user, 'User', user.name , applicable = ["Chat Room", "Chat Message"])
|
||||
create = add_user_permissions(param)
|
||||
param = get_params(user,'User', user.name, applicable = ["Chat Room", "Chat Message"])
|
||||
|
||||
# Initially create User Permission document with apply_to_all checked
|
||||
is_created = add_user_permissions(get_params(user, 'User', user.name))
|
||||
|
||||
self.assertEquals(is_created, 1)
|
||||
|
||||
is_created = add_user_permissions(param)
|
||||
frappe.db.commit()
|
||||
|
||||
removed_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user))
|
||||
created_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room"))
|
||||
created_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message"))
|
||||
is_created_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room"))
|
||||
is_created_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message"))
|
||||
|
||||
# Check that apply_to_all is removed
|
||||
self.assertIsNone(removed_apply_to_all)
|
||||
self.assertIsNotNone(created_applicable_first)
|
||||
self.assertIsNotNone(created_applicable_second)
|
||||
self.assertEquals(create, 1)
|
||||
|
||||
# Check that User Permissions for applicable is created
|
||||
self.assertIsNotNone(is_created_applicable_first)
|
||||
self.assertIsNotNone(is_created_applicable_second)
|
||||
self.assertEquals(is_created, 1)
|
||||
|
||||
def test_for_apply_to_all_on_update_from_applicable(self):
|
||||
''' Update User Permission from some to all applicable Doctypes'''
|
||||
user = create_user('test_bulk_creation_update@example.com')
|
||||
param = get_params(user, 'User', user.name)
|
||||
created = add_user_permissions(param)
|
||||
created_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user))
|
||||
|
||||
# create User permissions that with applicable
|
||||
is_created = add_user_permissions(get_params(user, 'User', user.name, applicable = ["Chat Room", "Chat Message"]))
|
||||
|
||||
self.assertEquals(is_created, 1)
|
||||
|
||||
is_created = add_user_permissions(param)
|
||||
is_created_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user))
|
||||
removed_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room"))
|
||||
removed_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message"))
|
||||
|
||||
# To check that a User permission with apply_to_all exists
|
||||
self.assertIsNotNone(is_created_apply_to_all)
|
||||
|
||||
self.assertIsNotNone(created_apply_to_all)
|
||||
# Check that all User Permission with applicable is removed
|
||||
self.assertIsNone(removed_applicable_first)
|
||||
self.assertIsNone(removed_applicable_second)
|
||||
self.assertEquals(created, 1)
|
||||
self.assertEquals(is_created, 1)
|
||||
|
||||
def create_user(email):
|
||||
''' create user with role system manager '''
|
||||
|
|
|
|||
|
|
@ -182,12 +182,17 @@ def add_user_permissions(data):
|
|||
data = frappe._dict(data)
|
||||
|
||||
d = check_applicable_doc_perm(data.user, data.doctype, data.docname)
|
||||
exists = frappe.db.exists("User Permission", {"user": data.user, "allow": data.doctype, "for_value": data.docname, "apply_to_all_doctypes": 1})
|
||||
exists = frappe.db.exists("User Permission", {
|
||||
"user": data.user,
|
||||
"allow": data.doctype,
|
||||
"for_value": data.docname,
|
||||
"apply_to_all_doctypes": 1
|
||||
})
|
||||
if data.apply_to_all_doctypes == 1 and not exists:
|
||||
remove_applicable(d, data.user, data.doctype, data.docname)
|
||||
insert_user_perm(data.user, data.doctype, data.docname, data.is_default, apply_to_all = 1)
|
||||
return 1
|
||||
else:
|
||||
elif len(data.applicable_doctypes) > 0 and data.apply_to_all_doctypes != 1:
|
||||
remove_apply_to_all(data.user, data.doctype, data.docname)
|
||||
update_applicable(d, data.applicable_doctypes, data.user, data.doctype, data.docname)
|
||||
for applicable in data.applicable_doctypes :
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ frappe.listview_settings['User Permission'] = {
|
|||
return data;
|
||||
}
|
||||
if(data.apply_to_all_doctypes == 0 && !("applicable_doctypes" in data)) {
|
||||
frappe.throw("Please select applicable Doctypes");
|
||||
frappe.throw(__("Please select applicable Doctypes"));
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
.chart-wrapper {
|
||||
border: 1px solid #d1d8dd;
|
||||
border-radius: 4px;
|
||||
height: 340px;
|
||||
height: 320px;
|
||||
margin: 15px 0;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: 15px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.chart-container > .title {
|
||||
.frappe-chart > text.title {
|
||||
margin: 0px;
|
||||
font-size: 14px;
|
||||
font-size: 14px !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.chart-loading-state {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
frappe.provide('frappe.dashboards');
|
||||
frappe.provide('frappe.dashboards.chart_sources');
|
||||
|
||||
|
||||
frappe.pages['dashboard'].on_page_load = function(wrapper) {
|
||||
var page = frappe.ui.make_app_page({
|
||||
parent: wrapper,
|
||||
|
|
@ -226,18 +227,18 @@ class DashboardChart {
|
|||
"Bar": "bar",
|
||||
};
|
||||
let chart_args = {
|
||||
title: this.chart_doc.chart_name.bold(),
|
||||
title: this.chart_doc.chart_name,
|
||||
data: this.data,
|
||||
type: chart_type_map[this.chart_doc.type],
|
||||
colors: [this.chart_doc.color || "light-blue"],
|
||||
axisOptions: {
|
||||
xIsSeries: this.chart_doc.timeseries
|
||||
},
|
||||
}
|
||||
};
|
||||
this.chart_container.find('.chart-loading-state').addClass('hide');
|
||||
|
||||
if(!this.chart) {
|
||||
this.chart = new Chart(this.chart_container.find(".chart-wrapper")[0], chart_args);
|
||||
this.chart = new frappe.Chart(this.chart_container.find(".chart-wrapper")[0], chart_args);
|
||||
} else {
|
||||
this.chart.update(this.data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ frappe.provide("frappe.customize_form");
|
|||
|
||||
frappe.ui.form.on("Customize Form", {
|
||||
onload: function(frm) {
|
||||
frappe.customize_form.add_fields_help(frm);
|
||||
|
||||
frm.set_query("doc_type", function() {
|
||||
return {
|
||||
translate_values: false,
|
||||
|
|
@ -206,103 +204,3 @@ frappe.customize_form.clear_locals_and_refresh = function(frm) {
|
|||
frm.refresh();
|
||||
}
|
||||
|
||||
frappe.customize_form.add_fields_help = function(frm) {
|
||||
$(frm.grids[0].parent).before(
|
||||
'<div style="padding: 10px">\
|
||||
<a id="fields_help" class="link_type">' + __("Help") + '</a>\
|
||||
</div>');
|
||||
$('#fields_help').click(function() {
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: __('Help: Field Properties'),
|
||||
width: 600
|
||||
});
|
||||
|
||||
var help =
|
||||
"<table cellspacing='25'>\
|
||||
<tr>\
|
||||
<td><b>" + __("Label") + "</b></td>\
|
||||
<td>" + __("Set the display label for the field") + "</td>\
|
||||
</tr>\
|
||||
<tr>\
|
||||
<td><b>" + __("Type") + "</b></td>\
|
||||
<td>" + __("Change type of field. (Currently, Type change is \
|
||||
allowed among 'Currency and Float')") + "</td>\
|
||||
</tr>\
|
||||
<tr>\
|
||||
<td width='25%'><b>" + __("Options") + "</b></td>\
|
||||
<td width='75%'>" + __("Specify the value of the field") + "</td>\
|
||||
</tr>\
|
||||
<tr>\
|
||||
<td><b>" + __("Perm Level") + "</b></td>\
|
||||
<td>\
|
||||
" + __("Assign a permission level to the field.") + "<br />\
|
||||
(" + __("Permissions can be managed via Setup > Role Permissions Manager") + "\
|
||||
</td>\
|
||||
</tr>\
|
||||
<tr>\
|
||||
<td><b>" + __("Width") + "</b></td>\
|
||||
<td>\
|
||||
" + __("Width of the input box") + "<br />\
|
||||
" + __("Example") + ": <i>120px</i>\
|
||||
</td>\
|
||||
</tr>\
|
||||
<tr>\
|
||||
<td><b>" + __("Reqd") + "</b></td>\
|
||||
<td>" + __("Mark the field as Mandatory") + "</td>\
|
||||
</tr>\
|
||||
<tr>\
|
||||
<td><b>" + __("In Filter") + "</b></td>\
|
||||
<td>" + __("Use the field to filter records") + "</td>\
|
||||
</tr>\
|
||||
<tr>\
|
||||
<td><b>" + __("Hidden") + "</b></td>\
|
||||
<td>" + __("Hide field in form") + "</td>\
|
||||
</tr>\
|
||||
<tr>\
|
||||
<td><b>" + __("Print Hide") + "</b></td>\
|
||||
<td>" + __("Hide field in Standard Print Format") + "</td>\
|
||||
</tr>\
|
||||
<tr>\
|
||||
<td><b>" + __("Report Hide") + "</b></td>\
|
||||
<td>" + __("Hide field in Report Builder") + "</td>\
|
||||
</tr>\
|
||||
<tr>\
|
||||
<td><b>" + __("Allow on Submit") + "</b></td>\
|
||||
<td>" + __("Allow field to remain editable even after submission") + "</td>\
|
||||
</tr>\
|
||||
<tr>\
|
||||
<td><b>" + __("Depends On") + "</b></td>\
|
||||
<td>\
|
||||
Show field if a condition is met<br />\
|
||||
Example: <code>eval:doc.status=='Cancelled'</code>\
|
||||
on a field like \"reason_for_cancellation\" will reveal \
|
||||
\"Reason for Cancellation\" only if the record is Cancelled.\
|
||||
</td>\
|
||||
</tr>\
|
||||
<tr>\
|
||||
<td><b>" + __("Description") + "</b></td>\
|
||||
<td>" + __("Show a description below the field") + "</td>\
|
||||
</tr>\
|
||||
<tr>\
|
||||
<td><b>" + __("Default") + "</b></td>\
|
||||
<td>" + __("Specify a default value") + "</td>\
|
||||
</tr>\
|
||||
<tr>\
|
||||
<td></td>\
|
||||
<td><a class='link_type' \
|
||||
onclick='frappe.customize_form.fields_help_dialog.hide()'\
|
||||
style='color:grey'>" + __("Press Esc to close") + "</a>\
|
||||
</td>\
|
||||
</tr>\
|
||||
</table>"
|
||||
|
||||
$y(d.body, {padding: '32px', textAlign: 'center', lineHeight: '200%'});
|
||||
|
||||
$a(d.body, 'div', '', {textAlign: 'left'}, help);
|
||||
|
||||
d.show();
|
||||
|
||||
frappe.customize_form.fields_help_dialog = d;
|
||||
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"doc_type",
|
||||
"properties",
|
||||
|
|
@ -16,6 +17,7 @@
|
|||
"quick_entry",
|
||||
"track_changes",
|
||||
"track_views",
|
||||
"allow_auto_repeat",
|
||||
"image_view",
|
||||
"column_break_5",
|
||||
"title_field",
|
||||
|
|
@ -59,17 +61,20 @@
|
|||
"label": "Max Attachments"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_copy",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Copy"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "istable",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Table",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "istable",
|
||||
"fieldname": "editable_grid",
|
||||
"fieldtype": "Check",
|
||||
|
|
@ -82,11 +87,13 @@
|
|||
"label": "Quick Entry"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "track_changes",
|
||||
"fieldtype": "Check",
|
||||
"label": "Track Changes"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.image_field",
|
||||
"fieldname": "image_view",
|
||||
"fieldtype": "Check",
|
||||
|
|
@ -150,16 +157,23 @@
|
|||
"options": "Customize Form Field"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "track_views",
|
||||
"fieldtype": "Check",
|
||||
"label": "Track Views"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_auto_repeat",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Auto Repeat"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "fa fa-glass",
|
||||
"idx": 1,
|
||||
"issingle": 1,
|
||||
"modified": "2019-05-13 18:54:40.610862",
|
||||
"modified": "2019-07-01 22:50:50.372465",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form",
|
||||
|
|
@ -177,6 +191,7 @@
|
|||
],
|
||||
"quick_entry": 1,
|
||||
"search_fields": "doc_type",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ from frappe.utils import cint
|
|||
from frappe.model.document import Document
|
||||
from frappe.model import no_value_fields, core_doctypes_list
|
||||
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
from frappe.model.docfield import supports_translation
|
||||
|
||||
doctype_properties = {
|
||||
|
|
@ -29,6 +30,7 @@ doctype_properties = {
|
|||
'max_attachments': 'Int',
|
||||
'track_changes': 'Check',
|
||||
'track_views': 'Check',
|
||||
'allow_auto_repeat': 'Check'
|
||||
}
|
||||
|
||||
docfield_properties = {
|
||||
|
|
@ -65,6 +67,7 @@ docfield_properties = {
|
|||
'columns': 'Int',
|
||||
'remember_last_selected_value': 'Check',
|
||||
'allow_bulk_edit': 'Check',
|
||||
'auto_repeat': 'Link'
|
||||
}
|
||||
|
||||
allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'),
|
||||
|
|
@ -108,6 +111,13 @@ class CustomizeForm(Document):
|
|||
translation = self.get_name_translation()
|
||||
self.label = translation.target_name if translation else ''
|
||||
|
||||
#If allow_auto_repeat is set, add auto_repeat custom field.
|
||||
if self.allow_auto_repeat:
|
||||
if not frappe.db.exists('Custom Field', {'fieldname': 'auto_repeat', 'dt': self.doc_type}):
|
||||
insert_after = self.fields[len(self.fields) - 1].fieldname
|
||||
df = dict(fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', options='Auto Repeat', insert_after=insert_after, read_only=1, no_copy=1, print_hide=1)
|
||||
create_custom_field(self.doc_type, df)
|
||||
|
||||
# NOTE doc is sent to clientside by run_method
|
||||
|
||||
def get_name_translation(self):
|
||||
|
|
@ -151,6 +161,7 @@ class CustomizeForm(Document):
|
|||
return
|
||||
|
||||
self.flags.update_db = False
|
||||
self.flags.rebuild_doctype_for_global_search = False
|
||||
|
||||
self.set_property_setters()
|
||||
self.update_custom_fields()
|
||||
|
|
@ -165,6 +176,10 @@ class CustomizeForm(Document):
|
|||
frappe.clear_cache(doctype=self.doc_type)
|
||||
self.fetch_to_customize()
|
||||
|
||||
if self.flags.rebuild_doctype_for_global_search:
|
||||
frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype',
|
||||
now=True, doctype=self.doc_type)
|
||||
|
||||
def set_property_setters(self):
|
||||
meta = frappe.get_meta(self.doc_type)
|
||||
# doctype property setters
|
||||
|
|
@ -225,6 +240,10 @@ class CustomizeForm(Document):
|
|||
frappe.msgprint(_("You can't set 'Translatable' for field {0}").format(df.label))
|
||||
continue
|
||||
|
||||
elif (property == 'in_global_search' and
|
||||
df.in_global_search != meta_df[0].get("in_global_search")):
|
||||
self.flags.rebuild_doctype_for_global_search = True
|
||||
|
||||
self.make_property_setter(property=property, value=df.get(property),
|
||||
property_type=docfield_properties[property], fieldname=df.fieldname)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from googleapiclient.errors import HttpError
|
|||
import time
|
||||
from datetime import datetime
|
||||
from frappe.utils import add_days, add_years
|
||||
from frappe.desk.doctype.event.event import has_permission
|
||||
|
||||
class CalendarConnector(BaseConnection):
|
||||
def __init__(self, connector):
|
||||
|
|
@ -64,24 +65,21 @@ class CalendarConnector(BaseConnection):
|
|||
|
||||
def insert(self, doctype, doc):
|
||||
if doctype == 'Events':
|
||||
from frappe.desk.doctype.event.event import has_permission
|
||||
d = frappe.get_doc("Event", doc["name"])
|
||||
if has_permission(d, self.account.name):
|
||||
if doc["start_datetime"] >= datetime.now():
|
||||
try:
|
||||
doctype = "Event"
|
||||
e = self.insert_events(doctype, doc)
|
||||
return e
|
||||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback(), "GCalendar Synchronization Error")
|
||||
try:
|
||||
doctype = "Event"
|
||||
e = self.insert_events(doctype, doc)
|
||||
return e
|
||||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback(), "GCalendar Synchronization Error")
|
||||
|
||||
|
||||
def update(self, doctype, doc, migration_id):
|
||||
if doctype == 'Events':
|
||||
from frappe.desk.doctype.event.event import has_permission
|
||||
d = frappe.get_doc("Event", doc["name"])
|
||||
if has_permission(d, self.account.name):
|
||||
if doc["start_datetime"] >= datetime.now() and migration_id is not None:
|
||||
if migration_id is not None:
|
||||
try:
|
||||
doctype = "Event"
|
||||
return self.update_events(doctype, doc, migration_id)
|
||||
|
|
@ -217,23 +215,23 @@ class CalendarConnector(BaseConnection):
|
|||
|
||||
day = []
|
||||
if e.repeat_on == "Every Day":
|
||||
if e.monday is not None:
|
||||
if e.monday == 1:
|
||||
day.append("MO")
|
||||
if e.tuesday is not None:
|
||||
if e.tuesday == 1:
|
||||
day.append("TU")
|
||||
if e.wednesday is not None:
|
||||
if e.wednesday == 1:
|
||||
day.append("WE")
|
||||
if e.thursday is not None:
|
||||
if e.thursday == 1:
|
||||
day.append("TH")
|
||||
if e.friday is not None:
|
||||
if e.friday == 1:
|
||||
day.append("FR")
|
||||
if e.saturday is not None:
|
||||
if e.saturday == 1:
|
||||
day.append("SA")
|
||||
if e.sunday is not None:
|
||||
if e.sunday == 1:
|
||||
day.append("SU")
|
||||
|
||||
day = "BYDAY=" + ",".join(str(d) for d in day)
|
||||
frequency = "FREQ=DAILY"
|
||||
frequency = "FREQ=WEEKLY"
|
||||
|
||||
elif e.repeat_on == "Every Week":
|
||||
frequency = "FREQ=WEEKLY"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
|||
import frappe, json, math
|
||||
from frappe.model.document import Document
|
||||
from frappe import _
|
||||
from frappe.utils import get_source_value
|
||||
from frappe.utils import get_source_value, cstr
|
||||
|
||||
class DataMigrationRun(Document):
|
||||
def run(self):
|
||||
|
|
@ -213,19 +213,19 @@ class DataMigrationRun(Document):
|
|||
def get_deleted_local_data(self):
|
||||
'''Fetch local deleted data using `frappe.get_all`. Used during Push'''
|
||||
mapping = self.get_mapping(self.current_mapping)
|
||||
or_filters = self.get_or_filters(mapping)
|
||||
filters = dict(
|
||||
deleted_doctype=mapping.local_doctype
|
||||
)
|
||||
filters = self.get_last_modified_condition()
|
||||
filters.update({
|
||||
"deleted_doctype": mapping.local_doctype
|
||||
})
|
||||
|
||||
data = frappe.get_all('Deleted Document', fields=['data'],
|
||||
filters=filters, or_filters=or_filters)
|
||||
data = frappe.get_all('Deleted Document', fields=['name', 'data'],
|
||||
filters=filters)
|
||||
|
||||
_data = []
|
||||
for d in data:
|
||||
doc = json.loads(d.data)
|
||||
if doc.get(mapping.migration_id_field):
|
||||
doc['_deleted_document_name'] = d.name
|
||||
doc['_deleted_document_name'] = d["name"]
|
||||
_data.append(doc)
|
||||
|
||||
return _data
|
||||
|
|
@ -306,8 +306,8 @@ class DataMigrationRun(Document):
|
|||
self.update_log('push_insert', 1)
|
||||
# post process after insert
|
||||
self.post_process_doc(local_doc=d, remote_doc=response_doc)
|
||||
except Exception:
|
||||
self.update_log('push_failed', d.name)
|
||||
except Exception as e:
|
||||
self.update_log('push_failed', {d.name: cstr(e)})
|
||||
|
||||
# update page_start
|
||||
self.db_set('current_mapping_start',
|
||||
|
|
@ -338,8 +338,8 @@ class DataMigrationRun(Document):
|
|||
self.update_log('push_update', 1)
|
||||
# post process after update
|
||||
self.post_process_doc(local_doc=d, remote_doc=response_doc)
|
||||
except Exception:
|
||||
self.update_log('push_failed', d.name)
|
||||
except Exception as e:
|
||||
self.update_log('push_failed', {d.name: cstr(e)})
|
||||
|
||||
# update page_start
|
||||
self.db_set('current_mapping_start',
|
||||
|
|
@ -370,8 +370,8 @@ class DataMigrationRun(Document):
|
|||
self.update_log('push_delete', 1)
|
||||
# post process only when action is success
|
||||
self.post_process_doc(local_doc=d, remote_doc=response_doc)
|
||||
except Exception:
|
||||
self.update_log('push_failed', d.name)
|
||||
except Exception as e:
|
||||
self.update_log('push_failed', {d.name: cstr(e)})
|
||||
|
||||
# update page_start
|
||||
self.db_set('current_mapping_start',
|
||||
|
|
@ -414,7 +414,7 @@ class DataMigrationRun(Document):
|
|||
self.post_process_doc(remote_doc=d, local_doc=local_doc)
|
||||
except Exception:
|
||||
# failed, append to log
|
||||
self.update_log('pull_failed', migration_id_value)
|
||||
self.update_log('pull_failed', {migration_id_value: cstr(e)})
|
||||
|
||||
if len(data) < mapping.page_length:
|
||||
# last page, done with pull
|
||||
|
|
|
|||
|
|
@ -178,10 +178,11 @@ class Database(object):
|
|||
frappe.errprint(("Execution time: {0} sec").format(round(time_end - time_start, 2)))
|
||||
|
||||
except Exception as e:
|
||||
if(frappe.conf.db_type == 'postgres'):
|
||||
if frappe.conf.db_type == 'postgres':
|
||||
self.rollback()
|
||||
|
||||
if frappe.conf.db_type == 'mariadb' and self.is_syntax_error(e):
|
||||
elif self.is_syntax_error(e):
|
||||
# only for mariadb
|
||||
frappe.errprint('Syntax error in query:')
|
||||
frappe.errprint(query)
|
||||
|
||||
|
|
@ -552,6 +553,10 @@ class Database(object):
|
|||
val = val[0][0] if val else None
|
||||
|
||||
df = frappe.get_meta(doctype).get_field(fieldname)
|
||||
|
||||
if not df:
|
||||
frappe.throw(_('Invalid field name: {0}').format(frappe.bold(fieldname)), self.InvalidColumnName)
|
||||
|
||||
if df.fieldtype in frappe.model.numeric_fieldtypes:
|
||||
val = cint(val)
|
||||
|
||||
|
|
@ -729,6 +734,7 @@ class Database(object):
|
|||
def commit(self):
|
||||
"""Commit current transaction. Calls SQL `COMMIT`."""
|
||||
self.sql("commit")
|
||||
|
||||
frappe.local.rollback_observers = []
|
||||
self.flush_realtime_log()
|
||||
enqueue_jobs_after_commit()
|
||||
|
|
@ -910,7 +916,7 @@ class Database(object):
|
|||
return self.is_missing_column(e) or self.is_missing_table(e)
|
||||
|
||||
def multisql(self, sql_dict, values=(), **kwargs):
|
||||
current_dialect = frappe.conf.db_type or 'mariadb'
|
||||
current_dialect = frappe.db.db_type or 'mariadb'
|
||||
query = sql_dict.get(current_dialect)
|
||||
return self.sql(query, values, **kwargs)
|
||||
|
||||
|
|
@ -922,18 +928,32 @@ class Database(object):
|
|||
conditions=conditions
|
||||
), values)
|
||||
else:
|
||||
frappe.throw('No conditions provided')
|
||||
frappe.throw(_('No conditions provided'))
|
||||
|
||||
def log_touched_tables(self, query, values=None):
|
||||
if values:
|
||||
query = frappe.safe_decode(self._cursor.mogrify(query, values))
|
||||
if query.strip().lower().split()[0] in ('insert', 'delete', 'update', 'alter'):
|
||||
# ([`\"']?) Captures ', " or ` at the begining of the table name (if provided)
|
||||
# single_word_regex is designed to match following patterns
|
||||
# `tabXxx`, tabXxx and "tabXxx"
|
||||
|
||||
# multi_word_regex is designed to match following patterns
|
||||
# `tabXxx Xxx` and "tabXxx Xxx"
|
||||
|
||||
# ([`"]?) Captures " or ` at the begining of the table name (if provided)
|
||||
# \1 matches the first captured group (quote character) at the end of the table name
|
||||
# multi word table name must have surrounding quotes.
|
||||
|
||||
# (tab([A-Z]\w+)( [A-Z]\w+)*) Captures table names that start with "tab"
|
||||
# and are continued with multiple words that start with a captital letter
|
||||
# e.g. 'tabXxx' or 'tabXxx Xxx' or 'tabXxx Xxx Xxx' and so on
|
||||
# \1 matches the first captured group (quote character) at the end of the table name
|
||||
tables = [groups[1] for groups in re.findall(r'([`"\']?)(tab([A-Z]\w+)( [A-Z]\w+)*)\1', query)]
|
||||
|
||||
single_word_regex = r'([`"]?)(tab([A-Z]\w+))\1'
|
||||
multi_word_regex = r'([`"])(tab([A-Z]\w+)( [A-Z]\w+)+)\1'
|
||||
tables = []
|
||||
for regex in (single_word_regex, multi_word_regex):
|
||||
tables += [groups[1] for groups in re.findall(regex, query)]
|
||||
|
||||
if frappe.flags.touched_tables is None:
|
||||
frappe.flags.touched_tables = set()
|
||||
frappe.flags.touched_tables.update(tables)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ class MariaDBDatabase(Database):
|
|||
REGEX_CHARACTER = 'regexp'
|
||||
|
||||
def setup_type_map(self):
|
||||
self.db_type = 'mariadb'
|
||||
self.type_map = {
|
||||
'Currency': ('decimal', '18,6'),
|
||||
'Int': ('int', '11'),
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ CREATE TABLE `tabDocField` (
|
|||
`unique` int(1) NOT NULL DEFAULT 0,
|
||||
`no_copy` int(1) NOT NULL DEFAULT 0,
|
||||
`allow_on_submit` int(1) NOT NULL DEFAULT 0,
|
||||
`show_preview_popup` int(1) NOT NULL DEFAULT 0,
|
||||
`trigger` varchar(255) DEFAULT NULL,
|
||||
`collapsible_depends_on` text,
|
||||
`depends_on` text,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class PostgresDatabase(Database):
|
|||
REGEX_CHARACTER = '~'
|
||||
|
||||
def setup_type_map(self):
|
||||
self.db_type = 'postgres'
|
||||
self.type_map = {
|
||||
'Currency': ('decimal', '18,6'),
|
||||
'Int': ('bigint', None),
|
||||
|
|
@ -110,13 +111,10 @@ class PostgresDatabase(Database):
|
|||
|
||||
def format_date(self, date):
|
||||
if not date:
|
||||
return '0001-01-01::DATE'
|
||||
return '0001-01-01'
|
||||
|
||||
if isinstance(date, frappe.string_types):
|
||||
if ':' not in date:
|
||||
date = date + '::DATE'
|
||||
else:
|
||||
date = date.strftime('%Y-%m-%d') + '::DATE'
|
||||
if not isinstance(date, frappe.string_types):
|
||||
date = date.strftime('%Y-%m-%d')
|
||||
|
||||
return date
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ CREATE TABLE "tabDocField" (
|
|||
"unique" smallint NOT NULL DEFAULT 0,
|
||||
"no_copy" smallint NOT NULL DEFAULT 0,
|
||||
"allow_on_submit" smallint NOT NULL DEFAULT 0,
|
||||
"show_preview_popup" smallint NOT NULL DEFAULT 0,
|
||||
"trigger" varchar(255) DEFAULT NULL,
|
||||
"collapsible_depends_on" text,
|
||||
"depends_on" text,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,20 @@ class PostgresTable(DBTable):
|
|||
query.append("ADD COLUMN `{}` {}".format(col.fieldname, col.get_definition()))
|
||||
|
||||
for col in self.change_type:
|
||||
query.append("ALTER COLUMN `{}` TYPE {}".format(col.fieldname, get_definition(col.fieldtype, precision=col.precision, length=col.length)))
|
||||
using_clause = ""
|
||||
if col.fieldtype in ("Datetime"):
|
||||
# The USING option of SET DATA TYPE can actually specify any expression
|
||||
# involving the old values of the row
|
||||
# read more https://www.postgresql.org/docs/9.1/sql-altertable.html
|
||||
using_clause = "USING {}::timestamp without time zone".format(col.fieldname)
|
||||
elif col.fieldtype in ("Check"):
|
||||
using_clause = "USING {}::smallint".format(col.fieldname)
|
||||
|
||||
query.append("ALTER COLUMN {0} TYPE {1} {2}".format(
|
||||
col.fieldname,
|
||||
get_definition(col.fieldtype, precision=col.precision, length=col.length),
|
||||
using_clause)
|
||||
)
|
||||
|
||||
for col in self.set_default:
|
||||
if col.fieldname=="name":
|
||||
|
|
@ -93,4 +106,4 @@ class PostgresTable(DBTable):
|
|||
fieldname, self.table_name)))
|
||||
raise e
|
||||
else:
|
||||
raise e
|
||||
raise e
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ def setup_database(force, source_sql, verbose):
|
|||
subprocess_env['PGPASSWORD'] = str(frappe.conf.db_password)
|
||||
# bootstrap db
|
||||
subprocess.check_output([
|
||||
'psql', frappe.conf.db_name, '-h', 'localhost', '-U',
|
||||
'psql', frappe.conf.db_name, '-h', frappe.conf.db_host, '-U',
|
||||
frappe.conf.db_name, '-f',
|
||||
os.path.join(os.path.dirname(__file__), 'framework_postgres.sql')
|
||||
], env=subprocess_env)
|
||||
|
|
|
|||
|
|
@ -1,143 +0,0 @@
|
|||
// Copyright (c) 2018, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
frappe.provide("frappe.auto_repeat");
|
||||
|
||||
frappe.ui.form.on('Auto Repeat', {
|
||||
setup: function(frm) {
|
||||
frm.fields_dict['reference_doctype'].get_query = function() {
|
||||
return {
|
||||
query: "frappe.desk.doctype.auto_repeat.auto_repeat.auto_repeat_doctype_query"
|
||||
};
|
||||
};
|
||||
|
||||
frm.fields_dict['reference_document'].get_query = function() {
|
||||
return {
|
||||
filters: {
|
||||
"docstatus": 1,
|
||||
"auto_repeat": ''
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
frm.fields_dict['print_format'].get_query = function() {
|
||||
return {
|
||||
filters: {
|
||||
"doc_type": frm.doc.reference_doctype
|
||||
}
|
||||
};
|
||||
};
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
||||
if(frm.doc.docstatus == 1) {
|
||||
|
||||
let label = __('View {0}', [__(frm.doc.reference_doctype)]);
|
||||
frm.add_custom_button(__(label),
|
||||
function() {
|
||||
frappe.route_options = {
|
||||
"auto_repeat": frm.doc.name,
|
||||
};
|
||||
frappe.set_route("List", frm.doc.reference_doctype);
|
||||
}
|
||||
);
|
||||
|
||||
if(frm.doc.status != 'Stopped') {
|
||||
frm.add_custom_button(__("Stop"),
|
||||
function() {
|
||||
frm.events.stop_resume_auto_repeat(frm, "Stopped");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if(frm.doc.status == 'Stopped') {
|
||||
frm.add_custom_button(__("Restart"),
|
||||
function() {
|
||||
frm.events.stop_resume_auto_repeat(frm, "Resumed");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if(frm.doc.docstatus!= 0 && !frm.doc.status.includes('Stopped', 'Cancelled') && frm.doc.next_schedule_date >= frappe.datetime.get_today()){
|
||||
frappe.auto_repeat.render_schedule(frm);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
stop_resume_auto_repeat: function(frm, status) {
|
||||
frappe.call({
|
||||
method: "frappe.desk.doctype.auto_repeat.auto_repeat.stop_resume_auto_repeat",
|
||||
args: {
|
||||
auto_repeat: frm.doc.name,
|
||||
status: status
|
||||
},
|
||||
callback: function(r) {
|
||||
if(r.message) {
|
||||
frm.set_value("status", r.message);
|
||||
frm.reload_doc();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
template: function(frm) {
|
||||
if (frm.doc.template) {
|
||||
frappe.model.with_doc("Email Template", frm.doc.template, () => {
|
||||
let email_template = frappe.get_doc("Email Template", frm.doc.template);
|
||||
frm.set_value("subject", email_template.subject);
|
||||
frm.set_value("message", email_template.response);
|
||||
frm.refresh_field("subject");
|
||||
frm.refresh_field("message");
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
get_contacts: function(frm) {
|
||||
frappe.call({
|
||||
method: "frappe.desk.doctype.auto_repeat.auto_repeat.get_contacts",
|
||||
args: {
|
||||
reference_doctype: frm.doc.reference_doctype,
|
||||
reference_name: frm.doc.reference_document
|
||||
},
|
||||
callback: function(r) {
|
||||
if(r.message) {
|
||||
frm.set_value("recipients", r.message.join());
|
||||
frm.refresh_field("recipients");
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
preview_message: function(frm) {
|
||||
if (frm.doc.message) {
|
||||
frappe.call({
|
||||
method: "frappe.desk.doctype.auto_repeat.auto_repeat.generate_message_preview",
|
||||
args: {
|
||||
reference_dt: frm.doc.reference_doctype,
|
||||
reference_doc: frm.doc.reference_document,
|
||||
subject: frm.doc.subject,
|
||||
message: frm.doc.message
|
||||
},
|
||||
callback: function(r) {
|
||||
if(r.message) {
|
||||
frappe.msgprint(r.message.message, r.message.subject)
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
frappe.msgprint(__("Please setup a message first"), __("Message not setup"))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frappe.auto_repeat.render_schedule = function(frm) {
|
||||
frappe.call({
|
||||
method: "get_auto_repeat_schedule",
|
||||
doc: frm.doc
|
||||
}).done((r) => {
|
||||
var wrapper = $(frm.fields_dict["auto_repeat_schedule"].wrapper);
|
||||
wrapper.html(frappe.render_template ("auto_repeat_schedule", {"schedule_details" : r.message || []} ));
|
||||
});
|
||||
frm.refresh_fields() ;
|
||||
};
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,408 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import calendar
|
||||
from frappe import _
|
||||
from frappe.desk.form import assign_to
|
||||
from frappe.utils.jinja import validate_template
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe.utils.user import get_system_managers
|
||||
from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day
|
||||
from frappe.model.document import Document
|
||||
from frappe.core.doctype.communication.email import make
|
||||
from frappe.utils.background_jobs import get_jobs
|
||||
|
||||
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
|
||||
|
||||
|
||||
class AutoRepeat(Document):
|
||||
def onload(self):
|
||||
self.set_onload("auto_repeat_schedule", self.get_auto_repeat_schedule())
|
||||
|
||||
def validate(self):
|
||||
self.update_status()
|
||||
self.validate_reference_doctype()
|
||||
self.validate_dates()
|
||||
self.validate_next_schedule_date()
|
||||
self.validate_email_id()
|
||||
self.link_party()
|
||||
|
||||
validate_template(self.subject or "")
|
||||
validate_template(self.message or "")
|
||||
|
||||
def before_submit(self):
|
||||
if not self.next_schedule_date:
|
||||
self.next_schedule_date = get_next_schedule_date(
|
||||
self.start_date, self.frequency, self.repeat_on_day)
|
||||
|
||||
def on_submit(self):
|
||||
self.update_auto_repeat_id()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
self.validate_dates()
|
||||
self.set_next_schedule_date()
|
||||
|
||||
def before_cancel(self):
|
||||
self.unlink_auto_repeat_id()
|
||||
self.next_schedule_date = None
|
||||
|
||||
def unlink_auto_repeat_id(self):
|
||||
frappe.db.sql(
|
||||
"update `tab{0}` set auto_repeat = null where auto_repeat=%s".format(self.reference_doctype), self.name)
|
||||
|
||||
def validate_reference_doctype(self):
|
||||
if not frappe.get_meta(self.reference_doctype).has_field('auto_repeat'):
|
||||
frappe.throw(_("Add custom field Auto Repeat in the doctype {0}").format(self.reference_doctype))
|
||||
|
||||
def validate_dates(self):
|
||||
if self.end_date and getdate(self.start_date) > getdate(self.end_date):
|
||||
frappe.throw(_("End date must be greater than start date"))
|
||||
|
||||
def validate_next_schedule_date(self):
|
||||
if self.repeat_on_day and self.next_schedule_date:
|
||||
next_date = getdate(self.next_schedule_date)
|
||||
if next_date.day != self.repeat_on_day:
|
||||
# if the repeat day is the last day of the month (31)
|
||||
# and the current month does not have as many days,
|
||||
# then the last day of the current month is a valid date
|
||||
lastday = calendar.monthrange(next_date.year, next_date.month)[1]
|
||||
if self.repeat_on_day < lastday:
|
||||
# the specified day of the month is not same as the day specified
|
||||
# or the last day of the month
|
||||
frappe.throw(_("Next Date's day and Repeat on Day of Month must be equal"))
|
||||
|
||||
def validate_email_id(self):
|
||||
if self.notify_by_email:
|
||||
if self.recipients:
|
||||
email_list = split_emails(self.recipients.replace("\n", ""))
|
||||
|
||||
from frappe.utils import validate_email_address
|
||||
for email in email_list:
|
||||
if not validate_email_address(email):
|
||||
frappe.throw(_("{0} is an invalid email address in 'Recipients'").format(email))
|
||||
else:
|
||||
frappe.throw(_("'Recipients' not specified"))
|
||||
|
||||
def set_next_schedule_date(self):
|
||||
if self.repeat_on_day:
|
||||
self.next_schedule_date = get_next_date(self.next_schedule_date, 0, self.repeat_on_day)
|
||||
|
||||
def update_auto_repeat_id(self):
|
||||
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name)
|
||||
|
||||
def update_status(self, status=None):
|
||||
self.status = {
|
||||
'0': 'Draft',
|
||||
'1': 'Submitted',
|
||||
'2': 'Cancelled'
|
||||
}[cstr(self.docstatus or 0)]
|
||||
|
||||
if status and status != 'Resumed':
|
||||
self.status = status
|
||||
|
||||
def get_auto_repeat_schedule(self):
|
||||
schedule_details = []
|
||||
start_date_copy = getdate(self.start_date)
|
||||
end_date_copy = getdate(self.end_date)
|
||||
today_copy = frappe.utils.datetime.date.today()
|
||||
|
||||
if start_date_copy < today_copy:
|
||||
start_date_copy = today_copy
|
||||
|
||||
if not self.end_date:
|
||||
days = 60 if self.frequency in ['Daily', 'Weekly'] else 365
|
||||
end_date_copy = add_days(today_copy, days)
|
||||
|
||||
while (getdate(start_date_copy) < getdate(end_date_copy)):
|
||||
start_date_copy = get_next_schedule_date(start_date_copy, self.frequency, self.repeat_on_day)
|
||||
row = {
|
||||
"reference_document" : self.reference_document,
|
||||
"frequency" : self.frequency,
|
||||
"next_scheduled_date" : start_date_copy
|
||||
}
|
||||
schedule_details.append(row)
|
||||
|
||||
return schedule_details
|
||||
|
||||
def link_party(self):
|
||||
reference = frappe.get_meta(self.reference_doctype)
|
||||
for field in reference.fields:
|
||||
if field.options in ['Customer', 'Supplier', 'Employee']:
|
||||
self.reference_party_doctype = field.options
|
||||
self.reference_party = frappe.db.get_value(self.reference_doctype, self.reference_document, field.fieldname)
|
||||
break
|
||||
|
||||
def get_next_schedule_date(start_date, frequency, repeat_on_day):
|
||||
mcount = month_map.get(frequency)
|
||||
if mcount:
|
||||
next_date = get_next_date(start_date, mcount, repeat_on_day)
|
||||
else:
|
||||
days = 7 if frequency == 'Weekly' else 1
|
||||
next_date = add_days(start_date, days)
|
||||
return next_date
|
||||
|
||||
def make_auto_repeat_entry(date=None):
|
||||
enqueued_method = 'frappe.desk.doctype.auto_repeat.auto_repeat.create_repeated_entries'
|
||||
jobs = get_jobs()
|
||||
|
||||
if not jobs or enqueued_method not in jobs[frappe.local.site]:
|
||||
date = date or today()
|
||||
for data in get_auto_repeat_entries(date):
|
||||
frappe.enqueue(enqueued_method, data=data)
|
||||
|
||||
def create_repeated_entries(data):
|
||||
schedule_date = getdate(data.next_schedule_date)
|
||||
while schedule_date <= getdate(today()) and not frappe.db.get_value('Auto Repeat', data.name, 'disabled'):
|
||||
create_documents(data, schedule_date)
|
||||
schedule_date = get_next_schedule_date(schedule_date, data.frequency, data.repeat_on_day)
|
||||
|
||||
if schedule_date and not frappe.db.get_value('Auto Repeat', data.name, 'disabled'):
|
||||
frappe.db.set_value('Auto Repeat', data.name, 'next_schedule_date', schedule_date)
|
||||
frappe.db.commit()
|
||||
|
||||
def get_auto_repeat_entries(date):
|
||||
return frappe.db.sql(""" select * from `tabAuto Repeat`
|
||||
where docstatus = 1 and next_schedule_date <=%s
|
||||
and reference_document is not null and reference_document != ''
|
||||
and next_schedule_date <= ifnull(end_date, '2199-12-31')
|
||||
and disabled = 0 and status != 'Stopped' """, (date), as_dict=1)
|
||||
|
||||
def create_documents(data, schedule_date):
|
||||
try:
|
||||
doc = make_new_document(data, schedule_date)
|
||||
if data.notify_by_email and data.recipients:
|
||||
print_format = data.print_format or "Standard"
|
||||
send_notification(doc, data, print_format=print_format)
|
||||
|
||||
frappe.db.commit()
|
||||
except Exception:
|
||||
frappe.db.rollback()
|
||||
frappe.db.begin()
|
||||
frappe.log_error(frappe.get_traceback(), _("Recurring document creation failure"))
|
||||
disable_auto_repeat(data)
|
||||
frappe.db.commit()
|
||||
if data.reference_document and not frappe.flags.in_test:
|
||||
notify_error_to_user(data)
|
||||
|
||||
def disable_auto_repeat(data):
|
||||
auto_repeat = frappe.get_doc('Auto Repeat', data.name)
|
||||
auto_repeat.db_set('disabled', 1)
|
||||
|
||||
def notify_error_to_user(data):
|
||||
party = ''
|
||||
party_type = ''
|
||||
|
||||
if data.reference_doctype in ['Sales Order', 'Sales Invoice', 'Delivery Note']:
|
||||
party_type = 'customer'
|
||||
elif data.reference_doctype in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']:
|
||||
party_type = 'supplier'
|
||||
|
||||
if party_type:
|
||||
party = frappe.db.get_value(data.reference_doctype, data.reference_document, party_type)
|
||||
|
||||
notify_errors(data.reference_document, data.reference_doctype, party, data.owner, data.name)
|
||||
|
||||
def make_new_document(args, schedule_date):
|
||||
doc = frappe.get_doc(args.reference_doctype, args.reference_document)
|
||||
new_doc = frappe.copy_doc(doc, ignore_no_copy=False)
|
||||
update_doc(new_doc, doc, args, schedule_date)
|
||||
new_doc.insert(ignore_permissions=True)
|
||||
|
||||
if args.submit_on_creation:
|
||||
new_doc.submit()
|
||||
|
||||
return new_doc
|
||||
|
||||
def update_doc(new_document, reference_doc, args, schedule_date):
|
||||
new_document.docstatus = 0
|
||||
if new_document.meta.get_field('set_posting_time'):
|
||||
new_document.set('set_posting_time', 1)
|
||||
|
||||
mcount = month_map.get(args.frequency)
|
||||
|
||||
if new_document.meta.get_field('auto_repeat'):
|
||||
new_document.set('auto_repeat', args.name)
|
||||
|
||||
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time',
|
||||
'select_print_heading', 'remarks', 'owner']:
|
||||
if new_document.meta.get_field(fieldname):
|
||||
new_document.set(fieldname, reference_doc.get(fieldname))
|
||||
|
||||
# copy item fields
|
||||
if new_document.meta.get_field('items'):
|
||||
for i, item in enumerate(new_document.items):
|
||||
for fieldname in ("page_break",):
|
||||
item.set(fieldname, reference_doc.items[i].get(fieldname))
|
||||
|
||||
for data in new_document.meta.fields:
|
||||
if data.fieldtype == 'Date' and data.reqd:
|
||||
new_document.set(data.fieldname, schedule_date)
|
||||
|
||||
set_auto_repeat_period(args, mcount, new_document)
|
||||
|
||||
new_document.run_method("on_recurring", reference_doc=reference_doc, auto_repeat_doc=args)
|
||||
|
||||
def set_auto_repeat_period(args, mcount, new_document):
|
||||
if mcount and new_document.meta.get_field('from_date') and new_document.meta.get_field('to_date'):
|
||||
last_ref_doc = frappe.db.sql("""
|
||||
select name, from_date, to_date
|
||||
from `tab{0}`
|
||||
where auto_repeat=%s and docstatus < 2
|
||||
order by creation desc
|
||||
limit 1
|
||||
""".format(args.reference_doctype), args.name, as_dict=1)
|
||||
|
||||
if not last_ref_doc:
|
||||
return
|
||||
|
||||
from_date = get_next_date(last_ref_doc[0].from_date, mcount)
|
||||
|
||||
if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and \
|
||||
(cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date)):
|
||||
to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount))
|
||||
else:
|
||||
to_date = get_next_date(last_ref_doc[0].to_date, mcount)
|
||||
|
||||
new_document.set('from_date', from_date)
|
||||
new_document.set('to_date', to_date)
|
||||
|
||||
def get_next_date(dt, mcount, day=None):
|
||||
dt = getdate(dt)
|
||||
dt += relativedelta(months=mcount, day=day)
|
||||
|
||||
return dt
|
||||
|
||||
def send_notification(new_rv, auto_repeat_doc, print_format='Standard'):
|
||||
"""Notify concerned persons about recurring document generation"""
|
||||
print_format = print_format
|
||||
subject = auto_repeat_doc.subject or ''
|
||||
message = auto_repeat_doc.message or ''
|
||||
|
||||
if not auto_repeat_doc.subject:
|
||||
subject = _("New {0}: #{1}").format(new_rv.doctype, new_rv.name)
|
||||
elif "{" in auto_repeat_doc.subject:
|
||||
subject = frappe.render_template(auto_repeat_doc.subject, {'doc': new_rv})
|
||||
|
||||
if not auto_repeat_doc.message:
|
||||
message = _("Please find attached {0} #{1}").format(new_rv.doctype, new_rv.name)
|
||||
elif "{" in auto_repeat_doc.message:
|
||||
message = frappe.render_template(auto_repeat_doc.message, {'doc': new_rv})
|
||||
|
||||
attachments = [frappe.attach_print(new_rv.doctype, new_rv.name,
|
||||
file_name=new_rv.name, print_format=print_format)]
|
||||
|
||||
make(doctype=new_rv.doctype, name=new_rv.name, recipients=auto_repeat_doc.recipients,
|
||||
subject=subject, content=message, attachments=attachments, send_email=1)
|
||||
|
||||
def notify_errors(doc, doctype, party, owner, name):
|
||||
recipients = get_system_managers(only_name=True)
|
||||
frappe.sendmail(recipients + [frappe.db.get_value("User", owner, "email")],
|
||||
subject=_("[Urgent] Error while creating recurring %s for %s" % (doctype, doc)),
|
||||
message=frappe.get_template("templates/emails/recurring_document_failed.html").render({
|
||||
"type": _(doctype),
|
||||
"name": doc,
|
||||
"party": party or "",
|
||||
"auto_repeat": name
|
||||
}))
|
||||
try:
|
||||
assign_task_to_owner(name, _("Recurring Documents Failed"), recipients)
|
||||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback(), _("Recurring Documents Failed"))
|
||||
|
||||
def assign_task_to_owner(name, msg, users):
|
||||
for d in users:
|
||||
args = {
|
||||
'doctype': 'Auto Repeat',
|
||||
'assign_to': d,
|
||||
'name': name,
|
||||
'description': msg,
|
||||
'priority': 'High'
|
||||
}
|
||||
assign_to.add(args)
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_auto_repeat(doctype, docname):
|
||||
doc = frappe.new_doc('Auto Repeat')
|
||||
|
||||
reference_doc = frappe.get_doc(doctype, docname)
|
||||
doc.reference_doctype = doctype
|
||||
doc.reference_document = docname
|
||||
doc.start_date = reference_doc.get('posting_date') or reference_doc.get('transaction_date')
|
||||
return doc
|
||||
|
||||
@frappe.whitelist()
|
||||
def stop_resume_auto_repeat(auto_repeat, status):
|
||||
doc = frappe.get_doc('Auto Repeat', auto_repeat)
|
||||
frappe.msgprint(_("Auto Repeat has been {0}").format(status))
|
||||
if status == 'Resumed':
|
||||
doc.next_schedule_date = get_next_schedule_date(today(),
|
||||
doc.frequency, doc.repeat_on_day)
|
||||
|
||||
doc.update_status(status)
|
||||
doc.save()
|
||||
|
||||
return doc.status
|
||||
|
||||
def auto_repeat_doctype_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.db.sql("""select parent from `tabDocField`
|
||||
where fieldname = 'auto_repeat'
|
||||
and parent like %(txt)s
|
||||
order by
|
||||
if(locate(%(_txt)s, parent), locate(%(_txt)s, parent), 99999),
|
||||
parent
|
||||
limit %(start)s, %(page_len)s""".format(**{
|
||||
'key': searchfield,
|
||||
}), {
|
||||
'txt': "%%%s%%" % txt,
|
||||
'_txt': txt.replace("%", ""),
|
||||
'start': start,
|
||||
'page_len': page_len
|
||||
})
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_contacts(reference_doctype, reference_name):
|
||||
docfields = frappe.get_meta(reference_doctype).fields
|
||||
|
||||
contact_fields = []
|
||||
for field in docfields:
|
||||
if field.fieldtype == "Link" and field.options == "Contact":
|
||||
contact_fields.append(field.fieldname)
|
||||
|
||||
if contact_fields:
|
||||
contacts = []
|
||||
for contact_field in contact_fields:
|
||||
contacts.append(frappe.db.get_value(reference_doctype, reference_name, contact_field))
|
||||
else:
|
||||
return []
|
||||
|
||||
if contacts:
|
||||
emails = []
|
||||
for contact in contacts:
|
||||
emails.append(frappe.db.get_value("Contact", contact, "email_id"))
|
||||
|
||||
return emails
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_reference(docname, reference):
|
||||
try:
|
||||
frappe.db.set_value("Auto Repeat", docname, "reference_document", reference)
|
||||
return "success"
|
||||
except Exception as e:
|
||||
raise e
|
||||
return "error"
|
||||
|
||||
@frappe.whitelist()
|
||||
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
|
||||
doc = frappe.get_doc(reference_dt, reference_doc)
|
||||
subject_preview = _("Please add a subject to your email")
|
||||
msg_preview = frappe.render_template(message, {'doc': doc})
|
||||
if subject:
|
||||
subject_preview = frappe.render_template(subject, {'doc': doc})
|
||||
|
||||
return {'message': msg_preview, 'subject': subject_preview}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
frappe.listview_settings['Auto Repeat'] = {
|
||||
add_fields: ["next_schedule_date"],
|
||||
get_indicator: function(doc) {
|
||||
if(doc.disabled) {
|
||||
return [__("Disabled"), "red"];
|
||||
} else if(doc.next_schedule_date >= frappe.datetime.get_today() && doc.status != 'Stopped') {
|
||||
return [__("Active"), "green"];
|
||||
} else if(doc.docstatus === 0) {
|
||||
return [__("Draft"), "red", "docstatus,=,0"];
|
||||
} else if(doc.status === 'Stopped') {
|
||||
return [__("Stopped"), "red"];
|
||||
} else {
|
||||
return [__("Expired"), "darkgrey"];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -100,6 +100,7 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
// nothing is mandatory
|
||||
_df.reqd = 0;
|
||||
_df.default = null;
|
||||
_df.depends_on = null;
|
||||
_df.read_only = 0;
|
||||
_df.permlevel = 1;
|
||||
_df.hidden = 0;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
import frappe, json
|
||||
from frappe import _
|
||||
from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan
|
||||
from frappe.utils import nowdate, add_to_date, getdate, get_last_day
|
||||
from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate
|
||||
from frappe.model.document import Document
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -59,7 +60,7 @@ def get(chart_name, from_date=None, to_date=None, refresh = None):
|
|||
result = add_missing_values(result, timegrain, from_date, to_date)
|
||||
|
||||
return {
|
||||
"labels": [r[0].strftime('%Y-%m-%d') for r in result],
|
||||
"labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result],
|
||||
"datasets": [{
|
||||
"name": chart.name,
|
||||
"values": [r[1] for r in result]
|
||||
|
|
@ -85,7 +86,7 @@ def convert_to_dates(data, timegrain):
|
|||
def get_unit_function(datefield, timegrain):
|
||||
unit_function = ''
|
||||
if timegrain=='Daily':
|
||||
if frappe.conf.db_type == 'mariadb':
|
||||
if frappe.db.db_type == 'mariadb':
|
||||
unit_function = 'dayofyear({})'.format(datefield)
|
||||
else:
|
||||
unit_function = 'extract(doy from {datefield})'.format(
|
||||
|
|
@ -193,3 +194,12 @@ class DashboardChart(Document):
|
|||
def on_update(self):
|
||||
frappe.cache().delete_key('chart-data:{}'.format(self.name))
|
||||
|
||||
def validate(self):
|
||||
if self.chart_type != 'Custom':
|
||||
self.check_required_field()
|
||||
|
||||
def check_required_field(self):
|
||||
if not self.based_on:
|
||||
frappe.throw(_("Time series based on is required to create a dashboard chart"))
|
||||
if not self.document_type:
|
||||
frappe.throw(_("Document type is required to create a dashboard chart"))
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import unittest, frappe
|
||||
from frappe.utils import getdate
|
||||
from frappe.utils import getdate, formatdate
|
||||
from frappe.desk.doctype.dashboard_chart.dashboard_chart import (get,
|
||||
get_period_ending)
|
||||
|
||||
|
|
@ -56,7 +56,8 @@ class TestDashboardChart(unittest.TestCase):
|
|||
|
||||
result = get(chart_name ='Test Dashboard Chart', refresh = 1)
|
||||
for idx in range(13):
|
||||
month = str(cur_date.year) + '-' + str(cur_date.strftime('%m')) + '-' + str(calendar.monthrange(cur_date.year, cur_date.month)[1])
|
||||
month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
|
||||
month = formatdate(month.strftime('%Y-%m-%d'))
|
||||
self.assertEqual(result.get('labels')[idx], month)
|
||||
cur_date += relativedelta(months=1)
|
||||
|
||||
|
|
@ -87,7 +88,8 @@ class TestDashboardChart(unittest.TestCase):
|
|||
|
||||
result = get(chart_name ='Test Empty Dashboard Chart', refresh = 1)
|
||||
for idx in range(13):
|
||||
month = str(cur_date.year) + '-' + str(cur_date.strftime('%m')) + '-' + str(calendar.monthrange(cur_date.year, cur_date.month)[1])
|
||||
month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
|
||||
month = formatdate(month.strftime('%Y-%m-%d'))
|
||||
self.assertEqual(result.get('labels')[idx], month)
|
||||
cur_date += relativedelta(months=1)
|
||||
|
||||
|
|
@ -118,7 +120,8 @@ class TestDashboardChart(unittest.TestCase):
|
|||
|
||||
result = get(chart_name ='Test Empty Dashboard Chart 2', refresh = 1)
|
||||
for idx in range(13):
|
||||
month = str(cur_date.year) + '-' + str(cur_date.strftime('%m')) + '-' + str(calendar.monthrange(cur_date.year, cur_date.month)[1])
|
||||
month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
|
||||
month = formatdate(month.strftime('%Y-%m-%d'))
|
||||
self.assertEqual(result.get('labels')[idx], month)
|
||||
cur_date += relativedelta(months=1)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
import frappe, os
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.modules.export_file import export_to_files
|
||||
from frappe.modules import get_module_path, scrub
|
||||
|
|
|
|||
|
|
@ -43,10 +43,17 @@ class Event(Document):
|
|||
def sync_communication(self):
|
||||
if self.event_participants:
|
||||
for participant in self.event_participants:
|
||||
communication_name = frappe.db.get_value("Communication", dict(reference_doctype=self.doctype, reference_name=self.name, timeline_doctype=participant.reference_doctype, timeline_name=participant.reference_docname), "name")
|
||||
if communication_name:
|
||||
communication = frappe.get_doc("Communication", communication_name)
|
||||
self.update_communication(participant, communication)
|
||||
comms = frappe.get_list("Communication", filters=[
|
||||
["Communication", "reference_doctype", "=", self.doctype],
|
||||
["Communication", "reference_name", "=", self.name],
|
||||
["Communication Link", "link_doctype", "=", participant.reference_doctype],
|
||||
["Communication Link", "link_name", "=", participant.reference_docname]
|
||||
], fields=["name"])
|
||||
|
||||
if comms:
|
||||
for comm in comms:
|
||||
communication = frappe.get_doc("Communication", comm.name)
|
||||
self.update_communication(participant, communication)
|
||||
else:
|
||||
meta = frappe.get_meta(participant.reference_doctype)
|
||||
if hasattr(meta, "allow_events_in_timeline") and meta.allow_events_in_timeline==1:
|
||||
|
|
@ -62,12 +69,11 @@ class Event(Document):
|
|||
communication.subject = self.subject
|
||||
communication.content = self.description if self.description else self.subject
|
||||
communication.communication_date = self.starts_on
|
||||
communication.timeline_doctype = participant.reference_doctype
|
||||
communication.timeline_name = participant.reference_docname
|
||||
communication.reference_doctype = self.doctype
|
||||
communication.reference_name = self.name
|
||||
communication.communication_medium = communication_mapping[self.event_category] if self.event_category else ""
|
||||
communication.communication_medium = communication_mapping.get(self.event_category) if self.event_category else ""
|
||||
communication.status = "Linked"
|
||||
communication.add_link(participant.reference_doctype, participant.reference_docname)
|
||||
communication.save(ignore_permissions=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -76,9 +82,18 @@ def delete_communication(event, reference_doctype, reference_docname):
|
|||
if isinstance(event, string_types):
|
||||
event = json.loads(event)
|
||||
|
||||
communication_name = frappe.db.get_value("Communication", dict(reference_doctype=event["doctype"], reference_name=event["name"], timeline_doctype=deleted_participant.reference_doctype, timeline_name=deleted_participant.reference_docname), "name")
|
||||
if communication_name:
|
||||
deletion = frappe.get_doc("Communication", communication_name).delete()
|
||||
comms = frappe.get_list("Communication", filters=[
|
||||
["Communication", "reference_doctype", "=", event.get("doctype")],
|
||||
["Communication", "reference_name", "=", event.get("name")],
|
||||
["Communication Link", "link_doctype", "=", deleted_participant.reference_doctype],
|
||||
["Communication Link", "link_name", "=", deleted_participant.reference_docname]
|
||||
], fields=["name"])
|
||||
|
||||
if comms:
|
||||
deletion = []
|
||||
for comm in comms:
|
||||
delete = frappe.get_doc("Communication", comm.name).delete()
|
||||
deletion.append(delete)
|
||||
|
||||
return deletion
|
||||
|
||||
|
|
|
|||
|
|
@ -684,4 +684,4 @@
|
|||
"track_changes": 1,
|
||||
"track_seen": 1,
|
||||
"track_views": 0
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,11 @@ class ToDo(Document):
|
|||
|
||||
def on_trash(self):
|
||||
# unlink todo from linked comments
|
||||
frappe.db.sql("""update `tabCommunication` set link_doctype=null, link_name=null
|
||||
where link_doctype=%(doctype)s and link_name=%(name)s""", {"doctype": self.doctype, "name": self.name})
|
||||
frappe.db.sql("""
|
||||
delete from `tabCommunication Link`
|
||||
where link_doctype=%(doctype)s and link_name=%(name)s""", {
|
||||
"doctype": self.doctype, "name": self.name
|
||||
})
|
||||
|
||||
self.update_in_reference()
|
||||
|
||||
|
|
@ -94,7 +97,7 @@ def get_permission_query_conditions(user):
|
|||
if "System Manager" in frappe.get_roles(user):
|
||||
return None
|
||||
else:
|
||||
return """(tabToDo.owner = {user} or tabToDo.assigned_by = {user})"""\
|
||||
return """(`tabToDo`.owner = {user} or `tabToDo`.assigned_by = {user})"""\
|
||||
.format(user=frappe.db.escape(user))
|
||||
|
||||
def has_permission(doc, user):
|
||||
|
|
@ -108,4 +111,4 @@ def new_todo(description):
|
|||
frappe.get_doc({
|
||||
'doctype': 'ToDo',
|
||||
'description': description
|
||||
}).insert()
|
||||
}).insert()
|
||||
|
|
|
|||
|
|
@ -160,36 +160,59 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields=
|
|||
group_by=None, as_dict=True):
|
||||
'''Returns list of communications for a given document'''
|
||||
if not fields:
|
||||
fields = '''`name`, `communication_type`,`communication_medium`, `comment_type`,
|
||||
`communication_date`, `content`, `sender`, `sender_full_name`, `cc`, `bcc`,
|
||||
`creation`, `subject`, `delivery_status`, `_liked_by`,
|
||||
`timeline_doctype`, `timeline_name`, `reference_doctype`, `reference_name`,
|
||||
`link_doctype`, `link_name`, `read_by_recipient`, `rating`, 'Communication' AS `doctype`'''
|
||||
|
||||
conditions = '''communication_type = 'Communication'
|
||||
and (
|
||||
(reference_doctype=%(doctype)s and reference_name=%(name)s)
|
||||
or (
|
||||
(timeline_doctype=%(doctype)s and timeline_name=%(name)s)
|
||||
and (communication_type='Communication')
|
||||
)
|
||||
)'''
|
||||
|
||||
fields = '''
|
||||
C.name, C.communication_type, C.communication_medium,
|
||||
C.comment_type, C.communication_date, C.content,
|
||||
C.sender, C.sender_full_name, C.cc, C.bcc,
|
||||
C.creation AS creation, C.subject, C.delivery_status,
|
||||
C._liked_by, C.reference_doctype, C.reference_name,
|
||||
C.read_by_recipient, C.rating
|
||||
'''
|
||||
|
||||
conditions = ''
|
||||
if after:
|
||||
# find after a particular date
|
||||
conditions+= ' and creation > {0}'.format(after)
|
||||
conditions += '''
|
||||
AND C.creation > {0}
|
||||
'''.format(after)
|
||||
|
||||
if doctype=='User':
|
||||
conditions+= " and not (reference_doctype='User' and communication_type='Communication')"
|
||||
conditions += '''
|
||||
AND NOT (C.reference_doctype='User' AND C.communication_type='Communication')
|
||||
'''
|
||||
|
||||
communications = frappe.db.sql("""select {fields}
|
||||
from `tabCommunication`
|
||||
where {conditions} {group_by}
|
||||
order by creation desc LIMIT %(limit)s OFFSET %(start)s""".format(
|
||||
fields = fields, conditions=conditions, group_by=group_by or ""),
|
||||
{ "doctype": doctype, "name": name, "start": frappe.utils.cint(start), "limit": limit },
|
||||
as_dict=as_dict)
|
||||
# communications linked to reference_doctype
|
||||
part1 = '''
|
||||
SELECT {fields}
|
||||
FROM `tabCommunication` as C
|
||||
WHERE C.communication_type IN ('Communication', 'Feedback')
|
||||
AND (C.reference_doctype = %(doctype)s AND C.reference_name = %(name)s)
|
||||
{conditions}
|
||||
'''.format(fields=fields, conditions=conditions)
|
||||
|
||||
# communications linked in Timeline Links
|
||||
part2 = '''
|
||||
SELECT {fields}
|
||||
FROM `tabCommunication` as C
|
||||
INNER JOIN `tabCommunication Link` ON C.name=`tabCommunication Link`.parent
|
||||
WHERE C.communication_type IN ('Communication', 'Feedback')
|
||||
AND `tabCommunication Link`.link_doctype = %(doctype)s AND `tabCommunication Link`.link_name = %(name)s
|
||||
{conditions}
|
||||
'''.format(fields=fields, conditions=conditions)
|
||||
|
||||
communications = frappe.db.sql('''
|
||||
SELECT *
|
||||
FROM (({part1}) UNION ({part2})) AS combined
|
||||
{group_by}
|
||||
ORDER BY creation DESC
|
||||
LIMIT %(limit)s
|
||||
OFFSET %(start)s
|
||||
'''.format(part1=part1, part2=part2, group_by=(group_by or '')), dict(
|
||||
doctype=doctype,
|
||||
name=name,
|
||||
start=frappe.utils.cint(start),
|
||||
limit=limit
|
||||
), as_dict=as_dict)
|
||||
|
||||
return communications
|
||||
|
||||
|
|
@ -229,4 +252,4 @@ def get_view_logs(doctype, docname):
|
|||
|
||||
if view_logs:
|
||||
logs = view_logs
|
||||
return logs
|
||||
return logs
|
||||
|
|
@ -5,7 +5,7 @@ from __future__ import unicode_literals
|
|||
import frappe, json
|
||||
import frappe.desk.form.meta
|
||||
import frappe.desk.form.load
|
||||
from frappe.utils.html_utils import clean_email_html
|
||||
from frappe.utils.html_utils import sanitize_html
|
||||
from frappe.desk.form.document_follow import follow_document
|
||||
|
||||
from frappe import _
|
||||
|
|
@ -18,7 +18,6 @@ def remove_attach():
|
|||
file_name = frappe.form_dict.get('file_name')
|
||||
frappe.delete_doc('File', fid)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def validate_link():
|
||||
"""validate link when updated by user"""
|
||||
|
|
@ -64,7 +63,7 @@ def add_comment(reference_doctype, reference_name, content, comment_email):
|
|||
doctype = 'Comment',
|
||||
reference_doctype = reference_doctype,
|
||||
reference_name = reference_name,
|
||||
content = clean_email_html(content),
|
||||
content = sanitize_html(content),
|
||||
comment_email = comment_email,
|
||||
comment_type = 'Comment'
|
||||
)).insert(ignore_permissions = True)
|
||||
|
|
@ -84,27 +83,23 @@ def update_comment(name, content):
|
|||
doc.save(ignore_permissions=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_next(doctype, value, prev, filters=None, order_by="modified desc"):
|
||||
|
||||
prev = not int(prev)
|
||||
sort_field, sort_order = order_by.split(" ")
|
||||
def get_next(doctype, value, prev, filters, sort_order, sort_field):
|
||||
|
||||
prev = int(prev)
|
||||
if not filters: filters = []
|
||||
if isinstance(filters, string_types):
|
||||
filters = json.loads(filters)
|
||||
|
||||
# condition based on sort order
|
||||
condition = ">" if sort_order.lower()=="desc" else "<"
|
||||
# # condition based on sort order
|
||||
condition = ">" if sort_order.lower() == "asc" else "<"
|
||||
|
||||
# switch the condition
|
||||
if prev:
|
||||
condition = "<" if condition==">" else "<"
|
||||
else:
|
||||
sort_order = "asc" if sort_order.lower()=="desc" else "desc"
|
||||
sort_order = "asc" if sort_order.lower() == "desc" else "desc"
|
||||
condition = "<" if condition == ">" else ">"
|
||||
|
||||
# add condition for next or prev item
|
||||
if not order_by[0] in [f[1] for f in filters]:
|
||||
filters.append([doctype, sort_field, condition, value])
|
||||
# # add condition for next or prev item
|
||||
filters.append([doctype, sort_field, condition, frappe.get_value(doctype, value, sort_field)])
|
||||
|
||||
res = frappe.get_list(doctype,
|
||||
fields = ["name"],
|
||||
|
|
@ -124,4 +119,4 @@ def get_pdf_link(doctype, docname, print_format='Standard', no_letterhead=0):
|
|||
docname = docname,
|
||||
print_format = print_format,
|
||||
no_letterhead = no_letterhead
|
||||
)
|
||||
)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue