Merge branch develop into remove_user_limit

This commit is contained in:
deepeshgarg007 2019-07-28 19:55:28 +05:30
commit 87e3531281
248 changed files with 10714 additions and 10465 deletions

13
.mergify.yml Normal file
View 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

View file

@ -2,43 +2,88 @@ language: python
dist: trusty
sudo: required
python:
- 2.7
- 3.6
env:
- DB=mariadb
- DB=postgres
- TEST_TYPE=ui
services:
- mysql
addons:
postgresql: "9.5"
hosts:
- test_site
- test_site_postgres
- test_site_ui
mariadb: 10.3
postgresql: 9.5
git:
depth: 1
cache:
- pip
- npm
- yarn
matrix:
exclude:
- python: 2.7
env: DB=postgres
- python: 2.7
env: TEST_TYPE=ui
include:
- name: "Python 3.6 MariaDB"
python: 3.6
env: DB=mariadb TYPE=server
script: bench --site test_site run-tests --coverage
- name: "Python 3.6 PostgreSQL"
python: 3.6
env: DB=postgres TYPE=server
script: bench --site test_site run-tests --coverage
- name: "Cypress"
python: 3.6
env: DB=mariadb TYPE=ui
before_script: bench --site test_site execute frappe.utils.install.complete_setup_wizard
script: bench --site test_site run-ui-tests frappe --headless
- name: "Python 2.7 MariaDB"
python: 2.7
env: DB=mariadb TYPE=server
script: bench --site test_site run-tests --coverage
install:
- $TRAVIS_BUILD_DIR/.travis/install.sh
- cd ~
- source ./.nvm/nvm.sh
- nvm install v8.10.0
- git clone https://github.com/frappe/bench --depth 1
- pip install -e ./bench
- bench init frappe-bench --skip-assets --python $(which python) --frappe-path $TRAVIS_BUILD_DIR
- mkdir ~/frappe-bench/sites/test_site
- cp $TRAVIS_BUILD_DIR/.travis/$DB.json ~/frappe-bench/sites/test_site/site_config.json
- mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
- mysql -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
- mysql -u root -e "CREATE DATABASE test_frappe"
- mysql -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
- mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
- mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
- mysql -u root -e "FLUSH PRIVILEGES"
- psql -c "CREATE DATABASE test_frappe" -U postgres
- psql -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres
- wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
- tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
- sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
- sudo chmod o+x /usr/local/bin/wkhtmltopdf
- cd ./frappe-bench
- sed -i 's/watch:/# watch:/g' Procfile
- sed -i 's/schedule:/# schedule:/g' Procfile
- if [ $TYPE == "server" ]; then sed -i 's/socketio:/# socketio:/g' Procfile; fi
- if [ $TYPE == "server" ]; then sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile; fi
- if [ $TYPE == "ui" ]; then bench setup requirements --node; fi
before_script:
- cd ~/frappe-bench
- sed -i 's/9000/9001/g' sites/common_site_config.json
- bench start &
- sleep 10
script:
- $TRAVIS_BUILD_DIR/.travis/run-tests.sh
- bench --site test_site reinstall --yes
- bench build --app frappe
after_script:
- coveralls -b apps/frappe -d ../../sites/.coverage
- pip install python-coveralls
- coveralls -b apps/frappe -d ../../sites/.coverage

View file

@ -1,27 +0,0 @@
#!/bin/bash
set -e
sudo rm /etc/apt/sources.list.d/mongodb*.list
sudo rm /etc/apt/sources.list.d/docker.list
sudo apt-get install hhvm && rm -rf /home/travis/.kiex/
sudo apt-get purge -y mysql-common mysql-server mysql-client
source ~/.nvm/nvm.sh
nvm install v8.10.0
pip install python-coveralls
wget https://raw.githubusercontent.com/frappe/bench/master/playbooks/install.py
sudo python install.py --develop --user travis --without-bench-setup
sudo pip install -e ~/bench
rm $TRAVIS_BUILD_DIR/.git/shallow
cd ~/ && bench init frappe-bench --python $(which python) --frappe-path $TRAVIS_BUILD_DIR
if [[ $DB == 'mariadb' ]]; then
cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/
elif [[ $TEST_TYPE == 'ui' ]]; then
cp -r $TRAVIS_BUILD_DIR/test_sites/test_site_ui ~/frappe-bench/sites/
elif [[ $DB == 'postgres' ]]; then
cp -r $TRAVIS_BUILD_DIR/test_sites/test_site_postgres ~/frappe-bench/sites/
fi

14
.travis/mariadb.json Normal file
View file

@ -0,0 +1,14 @@
{
"db_host": "localhost",
"db_name": "test_frappe",
"db_password": "test_frappe",
"db_type": "mariadb",
"auto_email_id": "test@example.com",
"mail_server": "smtp.example.com",
"mail_login": "test@example.com",
"mail_password": "test",
"admin_password": "admin",
"root_login": "root",
"root_password": "travis",
"host_name": "http://test_site:8000"
}

14
.travis/postgres.json Normal file
View file

@ -0,0 +1,14 @@
{
"db_host": "localhost",
"db_name": "test_frappe",
"db_password": "test_frappe",
"db_type": "postgres",
"auto_email_id": "test@example.com",
"mail_server": "smtp.example.com",
"mail_login": "test@example.com",
"mail_password": "test",
"admin_password": "admin",
"root_login": "postgres",
"root_password": "travis",
"host_name": "http://test_site:8000"
}

View file

@ -1,30 +0,0 @@
#!/bin/bash
set -e
setup_mariadb_env() {
mysql -u root -ptravis -e "create database $1"
mysql -u root -ptravis -e "USE mysql; CREATE USER '$1'@'localhost' IDENTIFIED BY '$1'; FLUSH PRIVILEGES; "
mysql -u root -ptravis -e "USE mysql; GRANT ALL PRIVILEGES ON \`$1\`.* TO '$1'@'localhost';"
}
if [[ $DB == 'mariadb' ]]; then
setup_mariadb_env 'test_frappe'
bench --site test_site reinstall --yes
bench --site test_site scheduler disable
bench --site test_site run-tests --coverage
elif [[ $TEST_TYPE == 'ui' ]]; then
setup_mariadb_env 'test_site_ui'
bench --site test_site_ui reinstall --yes
bench --site test_site_ui execute frappe.utils.install.complete_setup_wizard
bench --site test_site_ui scheduler disable
cd apps/frappe && yarn && yarn cypress:run
elif [[ $DB == 'postgres' ]]; then
psql -c "CREATE DATABASE test_frappe;" -U postgres
psql -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe';" -U postgres
bench --site test_site_postgres reinstall --yes
bench --site test_site_postgres scheduler disable
bench --site test_site_postgres run-tests --coverage
fi

View file

@ -1,4 +1,5 @@
{
"baseUrl": "http://test_site_ui:8000",
"projectId": "92odwv"
"projectId": "92odwv",
"adminPassword": "admin"
}

View file

@ -1,7 +1,7 @@
context('Awesome Bar', () => {
before(() => {
cy.visit('/login');
cy.login('Administrator', 'qwe');
cy.login();
cy.visit('/desk');
});
@ -10,8 +10,9 @@ context('Awesome Bar', () => {
});
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');

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

View file

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

View file

@ -1,6 +1,6 @@
context('FileUploader', () => {
before(() => {
cy.login('Administrator', 'qwe');
cy.login();
cy.visit('/desk');
});

View file

@ -1,6 +1,6 @@
context('Form', () => {
before(() => {
cy.login('Administrator', 'qwe');
cy.login();
cy.visit('/desk');
});

View file

@ -1,9 +1,9 @@
context('List View', () => {
before(() => {
cy.login('Administrator', 'qwe');
cy.login();
cy.visit('/desk');
cy.window().its('frappe').then(frappe => {
frappe.call("frappe.tests.test_utils.setup_workflow");
frappe.call("frappe.tests.ui_test_helpers.setup_workflow");
});
cy.clear_cache();
});

View file

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

View file

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

View file

@ -1,6 +1,6 @@
context('Form', () => {
before(() => {
cy.login('Administrator', 'qwe');
cy.login();
cy.visit('/desk');
});

View file

@ -1,6 +1,6 @@
context('Recorder', () => {
before(() => {
cy.login('Administrator', 'qwe');
cy.login();
});
it('Navigate to Recorder', () => {

View file

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

View file

@ -1,6 +1,6 @@
context('Table MultiSelect', () => {
beforeEach(() => {
cy.login('Administrator', 'qwe');
cy.login();
});
let name = 'table multiselect' + Math.random().toString().slice(2, 8);

View file

@ -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}"]`;
@ -72,15 +101,9 @@ Cypress.Commands.add('clear_cache', () => {
});
});
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('dialog', (opts) => {
return cy.window().then(win => {
var d = new win.frappe.ui.Dialog(opts);
d.show();
return d;
});

View file

@ -17,7 +17,7 @@ from faker import Faker
from .exceptions import *
from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader)
# Hamless for Python 3
# Harmless for Python 3
# For Python 2 set default encoding to utf-8
if sys.version[0] == '2':
reload(sys)

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

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

View file

@ -0,0 +1,375 @@
# -*- 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):
if self.end_date:
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 = 'Daily', start_date = today(), 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}

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

View file

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

View file

@ -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.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)
@ -51,8 +50,11 @@ class TestAutoRepeat(unittest.TestCase):
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(), 2),
'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

View file

@ -62,7 +62,7 @@ def popen(command, *args, **kwargs):
return_ = proc.wait()
if raise_err:
if return_ and raise_err:
raise subprocess.CalledProcessError(return_, command)
return return_

View file

@ -39,6 +39,10 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
reinstall=False, db_type=None):
"""Install a new Frappe site"""
if not force and os.path.exists(site):
print('Site {0} already exists'.format(site))
sys.exit(1)
if not db_name:
db_name = '_' + hashlib.sha1(site.encode()).hexdigest()[:16]
@ -276,6 +280,12 @@ def reload_doctype(context, doctype):
finally:
frappe.destroy()
@click.command('add-to-hosts')
@pass_context
def add_to_hosts(context):
"Add site to hosts"
for site in context.sites:
frappe.commands.popen('echo 127.0.0.1\t{0} | sudo tee -a /etc/hosts'.format(site))
@click.command('use')
@click.argument('site')
@ -347,12 +357,13 @@ def uninstall(context, app, dry_run=False, yes=False):
@click.option('--root-login', default='root')
@click.option('--root-password')
@click.option('--archived-sites-path')
@click.option('--no-backup', is_flag=True, default=False)
@click.option('--force', help='Force drop-site even if an error is encountered', is_flag=True, default=False)
def drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False):
_drop_site(site, root_login, root_password, archived_sites_path, force)
def drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False, no_backup=False):
_drop_site(site, root_login, root_password, archived_sites_path, force, no_backup)
def _drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False):
def _drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False, no_backup=False):
"Remove site from database and filesystem"
from frappe.database import drop_user_and_database
from frappe.utils.backups import scheduled_backup
@ -361,7 +372,8 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path=
frappe.connect()
try:
scheduled_backup(ignore_files=False, force=True)
if not no_backup:
scheduled_backup(ignore_files=False, force=True)
except Exception as err:
if force:
pass
@ -490,10 +502,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))
@ -537,4 +546,5 @@ commands = [
browse,
start_recording,
stop_recording,
add_to_hosts
]

View file

@ -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, raise_err=True)
@click.command('run-setup-wizard-ui-test')
@click.option('--app', help="App to run tests on, leave blank for all apps")

View file

@ -92,11 +92,6 @@ def get_data():
"name": "Google Settings",
"description": _("Google API Settings."),
},
{
"type": "doctype",
"name": "Google Maps Settings",
"description": _("Google Maps integration"),
},
{
"type": "doctype",
"name": "GCalendar Settings",

View file

@ -169,11 +169,27 @@ def get_data():
"name": "Workflow Action",
"description": _("Actions for workflow (e.g. Approve, Cancel).")
},
]
},
{
"label": _("Automation"),
"icon": "fa fa-random",
"items": [
{
"type": "doctype",
"name": "Assignment Rule",
"description": _("Set up rules for user assignments.")
}
},
{
"type": "doctype",
"name": "Milestone",
"description": _("Tracks milestones on the lifecycle of a document if it undergoes multiple stages.")
},
{
"type": "doctype",
"name": "Auto Repeat",
"description": _("Automatically generates recurring documents.")
},
]
},
]

View file

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

View file

@ -529,7 +529,7 @@ def update_mins_to_first_communication(parent, communication):
if frappe.db.get_all('User', filters={'email': communication.sender,
'user_type': 'System User', 'enabled': 1}, limit=1):
first_responded_on = communication.creation
if parent.meta.has_field('first_responded_on'):
if parent.meta.has_field('first_responded_on') and communication.sent_or_received == "Sent":
parent.db_set('first_responded_on', first_responded_on)
parent.db_set('mins_to_first_response', round(time_diff_in_seconds(first_responded_on, parent.creation) / 60), 2)

View file

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

View file

@ -37,6 +37,7 @@
"allow_rename",
"allow_import",
"allow_events_in_timeline",
"allow_auto_repeat",
"view_settings",
"title_field",
"search_fields",
@ -81,6 +82,7 @@
"search_index": 1
},
{
"default": "0",
"depends_on": "eval:!doc.istable",
"description": "Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended.",
"fieldname": "is_submittable",
@ -88,6 +90,7 @@
"label": "Is Submittable"
},
{
"default": "0",
"description": "Child Tables are shown as a Grid in other DocTypes",
"fieldname": "istable",
"fieldtype": "Check",
@ -97,6 +100,7 @@
"oldfieldtype": "Check"
},
{
"default": "0",
"depends_on": "eval:!doc.istable",
"description": "Single Types have only one record no tables associated. Values are stored in tabSingles",
"fieldname": "issingle",
@ -135,6 +139,7 @@
"label": "Track Changes"
},
{
"default": "0",
"depends_on": "eval:!doc.istable",
"description": "If enabled, the document is marked as seen, the first time a user opens it",
"fieldname": "track_seen",
@ -150,11 +155,13 @@
"label": "Track Views"
},
{
"default": "0",
"fieldname": "custom",
"fieldtype": "Check",
"label": "Custom?"
},
{
"default": "0",
"fieldname": "beta",
"fieldtype": "Check",
"label": "Beta"
@ -236,6 +243,7 @@
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "hide_toolbar",
"fieldtype": "Check",
"label": "Hide Sidebar and Menu",
@ -243,6 +251,7 @@
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "allow_copy",
"fieldtype": "Check",
"label": "Hide Copy",
@ -250,6 +259,7 @@
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "allow_rename",
"fieldtype": "Check",
"label": "Allow Rename",
@ -257,15 +267,23 @@
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "allow_import",
"fieldtype": "Check",
"label": "Allow Import (via Data Import Tool)"
},
{
"default": "0",
"fieldname": "allow_events_in_timeline",
"fieldtype": "Check",
"label": "Allow events in timeline"
},
{
"default": "0",
"fieldname": "allow_auto_repeat",
"fieldtype": "Check",
"label": "Allow Auto Repeat"
},
{
"collapsible": 1,
"fieldname": "view_settings",
@ -329,6 +347,13 @@
"label": "Color"
},
{
"default": "0",
"fieldname": "show_preview_popup",
"fieldtype": "Check",
"label": "Show Preview Popup"
},
{
"default": "0",
"fieldname": "show_name_in_global_search",
"fieldtype": "Check",
"label": "Make \"name\" searchable in Global Search"
@ -354,6 +379,7 @@
"options": "Domain"
},
{
"default": "0",
"fieldname": "read_only",
"fieldtype": "Check",
"label": "User Cannot Search",
@ -361,6 +387,7 @@
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "in_create",
"fieldtype": "Check",
"label": "User Cannot Create",
@ -411,17 +438,11 @@
"fieldtype": "Select",
"label": "Database Engine",
"options": "InnoDB\nMyISAM"
},
{
"default": "0",
"fieldname": "show_preview_popup",
"fieldtype": "Check",
"label": "Show Preview Popup"
}
],
"icon": "fa fa-bolt",
"idx": 6,
"modified": "2019-05-16 14:58:33.405381",
"modified": "2019-07-04 23:23:17.174960",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
@ -454,4 +475,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View file

@ -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
@ -47,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()
@ -76,6 +78,7 @@ class DocType(Document):
validate_permissions(self)
self.make_amendable()
self.make_repeatable()
self.validate_website()
if not self.is_new():
@ -384,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'):
@ -526,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""",

View file

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

View file

@ -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)
@ -682,7 +683,7 @@ def get_web_image(file_url):
frappe.msgprint(_("Unable to read file format for {0}").format(file_url))
raise
image = Image.open(StringIO(r.content))
image = Image.open(StringIO(frappe.safe_decode(r.content)))
try:
filename, extn = file_url.rsplit("/", 1)[1].rsplit(".", 1)

View file

@ -1,173 +1,72 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 1,
"autoname": "field:language_code",
"beta": 0,
"creation": "2014-08-22 16:12:17.249590",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "language_code",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Language Code",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "language_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Language Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "flag",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Flag",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "based_on",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Based On",
"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": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-globe",
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-12-29 14:40:33.210645",
"modified_by": "Administrator",
"module": "Core",
"name": "Language",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "language_name",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "language_name",
"track_changes": 1,
"track_seen": 0
}
"allow_rename": 1,
"autoname": "field:language_code",
"creation": "2014-08-22 16:12:17.249590",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"language_code",
"language_name",
"flag",
"based_on"
],
"fields": [
{
"fieldname": "language_code",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Language Code",
"reqd": 1,
"unique": 1
},
{
"fieldname": "language_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Language Name",
"reqd": 1
},
{
"fieldname": "flag",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Flag"
},
{
"fieldname": "based_on",
"fieldtype": "Link",
"label": "Based On",
"options": "Language"
}
],
"icon": "fa fa-globe",
"modified": "2019-07-19 16:32:12.652550",
"modified_by": "Administrator",
"module": "Core",
"name": "Language",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"read": 1,
"role": "System Manager",
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Guest",
"share": 1
}
],
"search_fields": "language_name",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "language_name",
"track_changes": 1
}

View file

@ -12,6 +12,7 @@ from frappe.modules.export_file import export_to_files
from frappe.modules import make_boilerplate
from frappe.core.doctype.page.page import delete_custom_role
from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles
from frappe.desk.reportview import append_totals_row
from six import iteritems
@ -76,11 +77,6 @@ class Report(Document):
if not self.json:
self.json = '{}'
if self.json:
data = json.loads(self.json)
data["add_total_row"] = self.add_total_row
self.json = json.dumps(data)
def export_doc(self):
if frappe.flags.in_import:
return
@ -178,6 +174,9 @@ class Report(Document):
out = out + [list(d) for d in result]
if params.get('add_totals_row'):
out = append_totals_row(out)
if as_dict:
data = []
for row in out:

View file

@ -0,0 +1,29 @@
{
"creation": "2019-07-17 16:21:33.546379",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"ref_doctype"
],
"fields": [
{
"fieldname": "ref_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Document Type",
"options": "DocType"
}
],
"istable": 1,
"modified": "2019-07-21 13:22:25.752553",
"modified_by": "Administrator",
"module": "Core",
"name": "Session Default",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,10 @@
# -*- 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 SessionDefault(Document):
pass

View file

@ -0,0 +1,15 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
frappe.ui.form.on('Session Default Settings', {
refresh: function(frm) {
frm.set_query('ref_doctype', 'session_defaults', function() {
return {
filters: {
issingle: 0,
istable: 0
}
};
});
}
});

View file

@ -0,0 +1,39 @@
{
"creation": "2019-07-17 16:22:31.300991",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"session_defaults"
],
"fields": [
{
"fieldname": "session_defaults",
"fieldtype": "Table",
"label": "Session Defaults",
"options": "Session Default"
}
],
"issingle": 1,
"modified": "2019-07-19 16:04:33.971089",
"modified_by": "Administrator",
"module": "Core",
"name": "Session Default Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,44 @@
# -*- 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 import _
import json
from frappe.model.document import Document
class SessionDefaultSettings(Document):
pass
@frappe.whitelist()
def get_session_default_values():
settings = frappe.get_single('Session Default Settings')
fields = []
for default_values in settings.session_defaults:
reference_doctype = frappe.scrub(default_values.ref_doctype)
fields.append({
'fieldname': reference_doctype,
'fieldtype': 'Link',
'options': default_values.ref_doctype,
'label': _('Default {0}').format(_(default_values.ref_doctype)),
'default': frappe.defaults.get_user_default(reference_doctype)
})
return json.dumps(fields)
@frappe.whitelist()
def set_session_default_values(default_values):
if not frappe.flags.in_test:
default_values = json.loads(default_values)
for entry in default_values:
try:
frappe.defaults.set_user_default(entry, default_values.get(entry))
except Exception:
return
return "success"
#called on hook 'on_logout' to clear defaults for the session
def clear_session_defaults():
settings = frappe.get_single('Session Default Settings').session_defaults
for entry in settings:
frappe.defaults.clear_user_default(frappe.scrub(entry.ref_doctype))

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from frappe.core.doctype.session_default_settings.session_default_settings import set_session_default_values, clear_session_defaults
class TestSessionDefaultSettings(unittest.TestCase):
def test_set_session_default_settings(self):
frappe.set_user("Administrator")
settings = frappe.get_single("Session Default Settings")
settings.session_defaults = []
settings.append("session_defaults", {
"ref_doctype": "Role"
})
settings.save()
set_session_default_values({"role": "Website Manager"})
todo = frappe.get_doc(dict(doctype="ToDo", description="test session defaults set", assigned_by="Administrator")).insert()
self.assertEqual(todo.role, "Website Manager")
def test_clear_session_defaults(self):
clear_session_defaults()
todo = frappe.get_doc(dict(doctype="ToDo", description="test session defaults cleared", assigned_by="Administrator")).insert()
self.assertNotEqual(todo.role, "Website Manager")

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -553,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)
@ -924,7 +928,7 @@ 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:

View file

@ -48,7 +48,10 @@ class DbManager:
if not host:
host = self.get_current_host()
self.db.sql("GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'%s';" % (target, user, host))
if frappe.conf.get('rds_db', 0) == 1:
self.db.sql("GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EXECUTE ON `%s`.* TO '%s'@'%s';" % (target, user, host))
else:
self.db.sql("GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'%s';" % (target, user, host))
def flush_privileges(self):
self.db.sql("FLUSH PRIVILEGES")

View file

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

View file

@ -1,144 +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");
}
);
}
}
frm.toggle_display('auto_repeat_schedule', !in_list(['Stopped', 'Cancelled'], frm.doc.status));
if(frm.doc.start_date && !in_list(['Stopped', 'Cancelled'], frm.doc.status)){
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

View file

@ -1,418 +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 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):
start_date_copy = self.start_date
today_copy = add_days(today(), -1)
if start_date_copy <= today_copy:
start_date_copy = today_copy
if not self.next_schedule_date:
self.next_schedule_date = get_next_schedule_date(
start_date_copy, self.frequency, self.repeat_on_day)
def on_submit(self):
self.update_auto_repeat_id()
def on_cancel(self):
self.update_status()
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
if self.docstatus == 2:
self.db_set("status", self.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)
start_date_copy = get_next_schedule_date(start_date_copy, self.frequency, self.repeat_on_day)
while (getdate(start_date_copy) < getdate(end_date_copy)):
row = {
"reference_document" : self.reference_document,
"frequency" : self.frequency,
"next_scheduled_date" : start_date_copy
}
schedule_details.append(row)
start_date_copy = get_next_schedule_date(start_date_copy, self.frequency, self.repeat_on_day)
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}

View file

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

View file

@ -4,6 +4,7 @@
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, formatdate
from frappe.model.document import Document
@ -44,7 +45,7 @@ def get(chart_name, from_date=None, to_date=None, refresh = None):
'''.format(
unit_function = get_unit_function(chart.based_on, timegrain),
datefield = chart.based_on,
aggregate_function = chart.chart_type,
aggregate_function = get_aggregate_function(chart.chart_type),
value_field = chart.value_based_on or '1',
doctype = chart.document_type,
conditions = conditions,
@ -66,6 +67,14 @@ def get(chart_name, from_date=None, to_date=None, refresh = None):
}]
}
def get_aggregate_function(chart_type):
return {
"Sum": "SUM",
"Count": "COUNT",
"Average": "AVG"
}[chart_type]
def convert_to_dates(data, timegrain):
result = []
for d in data:
@ -199,6 +208,6 @@ class DashboardChart(Document):
def check_required_field(self):
if not self.based_on:
frappe.throw("Time series based on is required to create a dashboard chart")
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")
frappe.throw(_("Document type is required to create a dashboard chart"))

View file

@ -71,7 +71,7 @@ class Event(Document):
communication.communication_date = self.starts_on
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)

View file

@ -684,4 +684,4 @@
"track_changes": 1,
"track_seen": 1,
"track_views": 0
}
}

View file

@ -252,4 +252,4 @@ def get_view_logs(doctype, docname):
if view_logs:
logs = view_logs
return logs
return logs

View file

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

View file

@ -3,8 +3,6 @@
from __future__ import unicode_literals
import frappe
import json
@frappe.whitelist()
def get_list_settings(doctype):
@ -22,31 +20,37 @@ def set_list_settings(doctype, values):
doc = frappe.new_doc("List View Setting")
doc.name = doctype
frappe.clear_messages()
doc.update(json.loads(values))
doc.update(frappe.parse_json(values))
doc.save()
@frappe.whitelist()
def get_user_assignments_and_count(doctype, current_filters):
def get_group_by_count(doctype, current_filters, field):
current_filters = frappe.parse_json(current_filters)
subquery_condition = ''
if current_filters:
# get the subquery
subquery = frappe.get_all(doctype,
filters=current_filters, return_query = True)
subquery = frappe.get_all(doctype, filters=current_filters, return_query = True)
if field == 'assigned_to':
subquery_condition = ' and `tabToDo`.reference_name in ({subquery})'.format(subquery = subquery)
return frappe.db.sql("""select `tabToDo`.owner as name, count(*) as count
from
`tabToDo`, `tabUser`
where
`tabToDo`.status='Open' and
`tabToDo`.owner = `tabUser`.name and
`tabUser`.user_type = 'System User'
{subquery_condition}
group by
`tabToDo`.owner
order by
count desc
limit 50""".format(subquery_condition = subquery_condition), as_dict=True)
else :
return frappe.db.get_list(doctype,
filters=current_filters,
group_by=field,
fields=['count(*) as count', field + ' as name'],
order_by='count desc',
limit=50,
)
todo_list = frappe.db.sql("""select `tabToDo`.owner as name, count(*) as count
from
`tabToDo`, `tabUser`
where
`tabToDo`.status='Open' and
`tabToDo`.owner = `tabUser`.name and
`tabUser`.user_type = 'System User'
{subquery_condition}
group by
`tabToDo`.owner
order by
count desc
limit 50""".format(subquery_condition = subquery_condition), as_dict=True)
return todo_list

View file

@ -287,44 +287,141 @@ def get_onboard_items(app, module):
return onboard_items or fallback_items
@frappe.whitelist()
def get_links_for_module(app, module):
return [l.get('label') for l in get_links(app, module)]
def get_links(app, module):
try:
sections = get_config(app, frappe.scrub(module))
except ImportError:
return []
link_names = []
links = []
for section in sections:
for item in section["items"]:
link_names.append(item.get("label"))
return link_names
for item in section['items']:
links.append(item)
return links
@frappe.whitelist()
def hide_modules_from_desktop(modules):
def get_desktop_settings():
from frappe.config import get_modules_from_all_apps_for_user
all_modules = get_modules_from_all_apps_for_user()
home_settings = get_home_settings()
modules_by_name = {}
for m in all_modules:
modules_by_name[m['module_name']] = m
module_categories = ['Modules', 'Domains', 'Places', 'Administration']
user_modules_by_category = {}
user_saved_modules_by_category = home_settings.modules_by_category or {}
user_saved_links_by_module = home_settings.links_by_module or {}
def apply_user_saved_links(module):
module = frappe._dict(module)
all_links = get_links(module.app, module.module_name)
module_links_by_label = {}
for link in all_links:
module_links_by_label[link['label']] = link
if module.module_name in user_saved_links_by_module:
user_links = frappe.parse_json(user_saved_links_by_module[module.module_name])
module.links = [module_links_by_label[l] for l in user_links if l in module_links_by_label]
return module
for category in module_categories:
if category in user_saved_modules_by_category:
user_modules = user_saved_modules_by_category[category]
user_modules_by_category[category] = [apply_user_saved_links(modules_by_name[m]) \
for m in user_modules]
else:
user_modules_by_category[category] = [apply_user_saved_links(m) \
for m in all_modules if m.get('category') == category]
# filter out hidden modules
if home_settings.hidden_modules:
for category in user_modules_by_category:
hidden_modules = home_settings.hidden_modules or []
modules = user_modules_by_category[category]
user_modules_by_category[category] = [module for module in modules if module.module_name not in hidden_modules]
return user_modules_by_category
@frappe.whitelist()
def update_hidden_modules(category_map):
category_map = frappe.parse_json(category_map)
home_settings = get_home_settings()
saved_hidden_modules = home_settings.hidden_modules or []
for category in category_map:
config = frappe._dict(category_map[category])
saved_hidden_modules += config.removed or []
saved_hidden_modules = [d for d in saved_hidden_modules if d not in (config.added or [])]
home_settings.hidden_modules = saved_hidden_modules
set_home_settings(home_settings)
return get_desktop_settings()
@frappe.whitelist()
def update_modules_order(module_category, modules):
modules = frappe.parse_json(modules)
home_settings = frappe.db.get_value("User", frappe.session.user, 'home_settings')
home_settings = frappe.parse_json(home_settings or '{}')
home_settings['hidden_modules'] = modules
frappe.db.set_value('User', frappe.session.user, 'home_settings', json.dumps(home_settings))
return home_settings
home_settings = get_home_settings()
home_settings.modules_by_category = home_settings.modules_by_category or {}
home_settings.modules_by_category[module_category] = modules
set_home_settings(home_settings)
@frappe.whitelist()
def update_links_for_module(module_name, links):
home_settings = frappe.db.get_value("User", frappe.session.user, 'home_settings')
home_settings = frappe.parse_json(home_settings or '{}')
links = frappe.parse_json(links)
home_settings = get_home_settings()
home_settings.setdefault('links', {})
home_settings['links'].setdefault(module_name, None)
home_settings['links'][module_name] = links
home_settings.setdefault('links_by_module', {})
home_settings['links_by_module'].setdefault(module_name, None)
home_settings['links_by_module'][module_name] = links
set_home_settings(home_settings)
return get_desktop_settings()
@frappe.whitelist()
def get_options_for_show_hide_cards():
from frappe.config import get_modules_from_all_apps_for_user
all_modules = get_modules_from_all_apps_for_user()
home_settings = get_home_settings()
hidden_modules = home_settings.hidden_modules or []
options = []
for module in all_modules:
module = frappe._dict(module)
options.append({
'category': module.category,
'label': module.label,
'value': module.module_name,
'checked': module.module_name not in hidden_modules
})
return options
def set_home_settings(home_settings):
frappe.cache().hset('home_settings', frappe.session.user, home_settings)
frappe.db.set_value('User', frappe.session.user, 'home_settings', json.dumps(home_settings))
@frappe.whitelist()
def get_home_settings():
def get_from_db():
settings = frappe.db.get_value("User", frappe.session.user, 'home_settings')
return frappe.parse_json(settings or '{}')
home_settings = frappe.cache().hget('home_settings', frappe.session.user, get_from_db)
return home_settings

View file

@ -213,7 +213,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
}
setTimeout(function() {
// Reload
window.location.href = '';
window.location.href = '/desk';
}, 2000);
}

View file

@ -307,6 +307,7 @@ def export_query():
if isinstance(data.get("file_format_type"), string_types):
file_format_type = data["file_format_type"]
include_indentation = data["include_indentation"]
if isinstance(data.get("visible_idx"), string_types):
visible_idx = json.loads(data.get("visible_idx"))
else:
@ -318,7 +319,7 @@ def export_query():
columns = get_columns_dict(data.columns)
from frappe.utils.xlsxutils import make_xlsx
xlsx_data = build_xlsx_data(columns, data, visible_idx)
xlsx_data = build_xlsx_data(columns, data, visible_idx, include_indentation)
xlsx_file = make_xlsx(xlsx_data, "Query Report")
frappe.response['filename'] = report_name + '.xlsx'
@ -326,7 +327,7 @@ def export_query():
frappe.response['type'] = 'binary'
def build_xlsx_data(columns, data, visible_idx):
def build_xlsx_data(columns, data, visible_idx,include_indentation):
result = [[]]
# add column headings
@ -344,7 +345,7 @@ def build_xlsx_data(columns, data, visible_idx):
label = columns[idx]["label"]
fieldname = columns[idx]["fieldname"]
cell_value = row.get(fieldname, row.get(label, ""))
if 'indent' in row and idx == 0:
if cint(include_indentation) and 'indent' in row and idx == 0:
cell_value = (' ' * cint(row['indent'])) + cell_value
row_data.append(cell_value)
else:

View file

@ -185,6 +185,10 @@ def append_totals_row(data):
for i in range(len(row)):
if isinstance(row[i], (float, int)):
totals[i] = (totals[i] or 0) + row[i]
if not isinstance(totals[0], (int, float)):
totals[0] = 'Total'
data.append(totals)
return data

File diff suppressed because it is too large Load diff

View file

@ -18,8 +18,8 @@ app_email = "info@frappe.io"
docs_app = "frappe_io"
translation_contribution_url = "https://translate.erpnext.xyz/api/method/translator.api.add_translation"
translation_contribution_status = "https://translate.erpnext.xyz/api/method/translator.api.translation_status"
translation_contribution_url = "https://translate.erpnext.com/api/method/translator.api.add_translation"
translation_contribution_status = "https://translate.erpnext.com/api/method/translator.api.translation_status"
before_install = "frappe.utils.install.before_install"
after_install = "frappe.utils.install.after_install"
@ -78,6 +78,8 @@ on_session_creation = [
"frappe.utils.scheduler.reset_enabled_scheduler_events",
]
on_logout = "frappe.core.doctype.session_default_settings.session_default_settings.clear_session_defaults"
# permissions
permission_query_conditions = {
@ -173,6 +175,8 @@ scheduler_events = {
"frappe.desk.form.document_follow.send_daily_updates",
"frappe.social.doctype.energy_point_settings.energy_point_settings.allocate_review_points",
"frappe.integrations.doctype.google_contacts.google_contacts.sync",
"frappe.automation.doctype.auto_repeat.auto_repeat.make_auto_repeat_entry",
"frappe.automation.doctype.auto_repeat.auto_repeat.set_auto_repeat_as_completed"
],
"daily_long": [
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",
@ -227,6 +231,7 @@ bot_parsers = [
setup_wizard_exception = "frappe.desk.page.setup_wizard.setup_wizard.email_setup_wizard_exception"
before_migrate = ['frappe.patches.v11_0.sync_user_permission_doctype_before_migrate.execute']
after_migrate = ['frappe.website.doctype.website_theme.website_theme.generate_theme_files_if_not_exist']
otp_methods = ['OTP App','Email','SMS']
user_privacy_documents = [

View file

@ -37,7 +37,8 @@ frappe.ui.form.on('Dropbox Settings', {
},
take_backup: function(frm) {
if ((frm.doc.app_access_key && frm.doc.app_secret_key) || (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config)){
if (frm.doc.enabled && ((frm.doc.app_access_key && frm.doc.app_secret_key)
|| (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config))) {
frm.add_custom_button(__("Take Backup Now"), function(frm){
frappe.call({
method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backup",

View file

@ -73,6 +73,7 @@ def authorize_access(g_contact, reauthorize=None):
if "refresh_token" in r:
frappe.db.set_value("Google Contacts", google_contact.name, "refresh_token", r.get("refresh_token"))
frappe.db.commit()
frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = "/desk#Form/Google%20Contacts/{}".format(google_contact.name)
@ -93,6 +94,7 @@ def google_callback(client_id=None, redirect_uri=None, code=None):
else:
google_contact = frappe.cache().hget("google_contacts", "google_contact")
frappe.db.set_value("Google Contacts", google_contact, "authorization_code", code)
frappe.db.commit()
authorize_access(google_contact)
@ -133,23 +135,24 @@ def sync(g_contact=None):
for name in connection.get("names"):
if name.get("metadata").get("primary"):
for email in connection.get("emailAddresses"):
if not frappe.db.exists("Contact", {"email_id": email.get("value")}):
contacts_updated += 1
if connection.get("emailAddresses"):
for email in connection.get("emailAddresses"):
if not frappe.db.exists("Contact", {"email_id": email.get("value")}):
contacts_updated += 1
frappe.get_doc({
"doctype": "Contact",
"salutation": name.get("honorificPrefix") if name.get("honorificPrefix") else "",
"first_name": name.get("givenName") if name.get("givenName") else "",
"middle_name": name.get("middleName") if name.get("middleName") else "",
"last_name": name.get("familyName") if name.get("familyName") else "",
"email_id": email.get("value") if email.get("value") else "",
"designation": get_indexed_value(connection.get("organizations"), 0, "title"),
"phone": get_indexed_value(connection.get("phoneNumbers"), 0, "value"),
"mobile_no": get_indexed_value(connection.get("phoneNumbers"), 1, "value"),
"source": "Google Contacts",
"google_contacts_description": get_indexed_value(connection.get("organizations"), 0, "name")
}).insert(ignore_permissions=True)
frappe.get_doc({
"doctype": "Contact",
"salutation": name.get("honorificPrefix") if name.get("honorificPrefix") else "",
"first_name": name.get("givenName") if name.get("givenName") else "",
"middle_name": name.get("middleName") if name.get("middleName") else "",
"last_name": name.get("familyName") if name.get("familyName") else "",
"email_id": email.get("value") if email.get("value") else "",
"designation": get_indexed_value(connection.get("organizations"), 0, "title"),
"phone": get_indexed_value(connection.get("phoneNumbers"), 0, "value"),
"mobile_no": get_indexed_value(connection.get("phoneNumbers"), 1, "value"),
"source": "Google Contacts",
"google_contacts_description": get_indexed_value(connection.get("organizations"), 0, "name")
}).insert(ignore_permissions=True)
if g_contact:
return _("{0} Google Contacts synced.").format(contacts_updated) if contacts_updated > 0 else _("No new Google Contacts synced.")

View file

@ -1,5 +0,0 @@
// Copyright (c) 2017, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Google Maps Settings', {
});

View file

@ -1,159 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-10-16 17:13:05.684227",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "enabled",
"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": "Enabled",
"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,
"fieldname": "client_key",
"fieldtype": "Data",
"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": "Client Key",
"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,
"fieldname": "home_address",
"fieldtype": "Link",
"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": "Home Address",
"length": 0,
"no_copy": 0,
"options": "Address",
"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
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2018-08-21 14:53:09.170463",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Maps Settings",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View file

@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
class GoogleMapsSettings(Document):
def validate(self):
if self.enabled:
if not self.client_key:
frappe.throw(_("Client key is required"))
if not self.home_address:
frappe.throw(_("Home Address is required"))
def get_client(self):
if not self.enabled:
frappe.throw(_("Google Maps integration is not enabled"))
import googlemaps
try:
client = googlemaps.Client(key=self.client_key)
except Exception as e:
frappe.throw(e.message)
return client

View file

@ -1,23 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Google Maps Settings", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Google Maps
() => frappe.tests.make('Google Maps Settings', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View file

@ -1,8 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
class TestGoogleMapsSettings(unittest.TestCase):
pass

View file

@ -6,7 +6,8 @@
"enable",
"google_credentials",
"client_id",
"client_secret"
"client_secret",
"api_key"
],
"fields": [
{
@ -32,10 +33,15 @@
"fieldtype": "Password",
"in_list_view": 1,
"label": "Client Secret"
},
{
"fieldname": "api_key",
"fieldtype": "Data",
"label": "API Key"
}
],
"issingle": 1,
"modified": "2019-06-19 15:28:05.957380",
"modified": "2019-06-29 13:26:33.201060",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Settings",

View file

@ -0,0 +1,38 @@
{
"creation": "2019-05-29 01:24:29.585060",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"ldap_group",
"erpnext_role"
],
"fields": [
{
"fieldname": "ldap_group",
"fieldtype": "Data",
"in_list_view": 1,
"label": "LDAP Group",
"reqd": 1
},
{
"fieldname": "erpnext_role",
"fieldtype": "Link",
"in_list_view": 1,
"label": "ERPNext Role",
"options": "Role",
"reqd": 1
}
],
"istable": 1,
"modified": "2019-07-15 06:46:38.050408",
"modified_by": "Administrator",
"module": "Integrations",
"name": "LDAP Group Mapping",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}

View file

@ -0,0 +1,10 @@
# -*- 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 LDAPGroupMapping(Document):
pass

View file

@ -1,594 +1,215 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-09-22 04:16:48.829658",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "System",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enabled",
"ldap_server_url",
"column_break_4",
"base_dn",
"password",
"section_break_5",
"organizational_unit",
"default_role",
"ldap_search_string",
"ldap_email_field",
"ldap_username_field",
"column_break_11",
"ldap_first_name_field",
"ldap_middle_name_field",
"ldap_last_name_field",
"ldap_phone_field",
"ldap_mobile_field",
"ldap_security",
"ssl_tls_mode",
"require_trusted_certificate",
"column_break_17",
"local_private_key_file",
"local_server_certificate_file",
"local_ca_certs_file",
"ldap_group_mappings_section",
"ldap_group_field",
"ldap_groups"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"default": "0",
"fieldname": "enabled",
"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": "Enabled",
"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
"label": "Enabled"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "ldap_server_url",
"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": "LDAP Server Url",
"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,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "organizational_unit",
"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": "Organizational Unit",
"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,
"translatable": 0,
"unique": 0
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "base_dn",
"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": "Base Distinguished Name (DN)",
"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,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "password",
"fieldtype": "Password",
"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": "Password for Base DN",
"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,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "section_break_5",
"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,
"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
"label": "LDAP User Creation and Mapping"
},
{
"fieldname": "organizational_unit",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Organizational Unit for Users",
"reqd": 1
},
{
"fieldname": "default_role",
"fieldtype": "Link",
"label": "Default Role on Creation",
"options": "Role",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "ldap_search_string",
"fieldtype": "Data",
"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": "LDAP Search String",
"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,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "ldap_first_name_field",
"fieldtype": "Data",
"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": "LDAP First Name Field",
"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,
"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": "ldap_email_field",
"fieldtype": "Data",
"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": "LDAP Email Field",
"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,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "ldap_username_field",
"fieldtype": "Data",
"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": "LDAP Username Field",
"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,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "ldap_first_name_field",
"fieldtype": "Data",
"label": "LDAP First Name Field",
"reqd": 1
},
{
"fieldname": "ldap_middle_name_field",
"fieldtype": "Data",
"label": "LDAP Middle Name Field"
},
{
"fieldname": "ldap_last_name_field",
"fieldtype": "Data",
"label": "LDAP Last Name Field"
},
{
"fieldname": "ldap_phone_field",
"fieldtype": "Data",
"label": "LDAP Phone Field"
},
{
"fieldname": "ldap_mobile_field",
"fieldtype": "Data",
"label": "LDAP Mobile Field"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "ldap_security",
"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": "LDAP Security",
"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
"label": "LDAP Security"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Off",
"description": "",
"fetch_if_empty": 0,
"fieldname": "ssl_tls_mode",
"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": "SSL/TLS Mode",
"length": 0,
"no_copy": 0,
"options": "Off\nStartTLS",
"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
"options": "Off\nStartTLS"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "No",
"fetch_if_empty": 0,
"fieldname": "require_trusted_certificate",
"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": "Require Trusted Certificate",
"length": 0,
"no_copy": 0,
"options": "No\nYes",
"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,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "local_private_key_file",
"fieldtype": "Data",
"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": "Path to private Key File",
"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
"label": "Path to private Key File"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "local_server_certificate_file",
"fieldtype": "Data",
"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": "Path to Server Certificate",
"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
"label": "Path to Server Certificate"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "local_ca_certs_file",
"fieldtype": "Data",
"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": "Path to CA Certs File",
"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
"label": "Path to CA Certs File"
},
{
"fieldname": "ldap_group_mappings_section",
"fieldtype": "Section Break",
"label": "LDAP Group Mappings"
},
{
"fieldname": "ldap_group_field",
"fieldtype": "Data",
"label": "LDAP Group Field"
},
{
"fieldname": "ldap_groups",
"fieldtype": "Table",
"label": "LDAP Group Mappings",
"options": "LDAP Group Mapping"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2019-04-29 10:56:42.322696",
"modified": "2019-07-15 06:48:16.562109",
"modified_by": "Administrator",
"module": "Integrations",
"name": "LDAP Settings",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 1,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

View file

@ -15,155 +15,195 @@ class LDAPSettings(Document):
if not self.flags.ignore_mandatory:
if self.ldap_search_string and self.ldap_search_string.endswith("={0}"):
connect_to_ldap(server_url=self.ldap_server_url,
base_dn=self.base_dn,
password=self.get_password(raise_exception=False),
ssl_tls_mode=self.ssl_tls_mode,
trusted_cert=self.require_trusted_certificate,
private_key_file=self.local_private_key_file,
server_cert_file=self.local_server_certificate_file,
ca_certs_file=self.local_ca_certs_file
)
self.connect_to_ldap(base_dn=self.base_dn, password=self.get_password(raise_exception=False))
else:
frappe.throw(_("LDAP Search String needs to end with a placeholder, eg sAMAccountName={0}"))
def connect_to_ldap(self, base_dn, password):
try:
import ldap3
import ssl
def get_ldap_client_settings():
#return the settings to be used on the client side.
result = {
"enabled": False
}
settings = frappe.get_doc("LDAP Settings")
if self.require_trusted_certificate == 'Yes':
tls_configuration = ldap3.Tls(validate=ssl.CERT_REQUIRED, version=ssl.PROTOCOL_TLSv1)
else:
tls_configuration = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1)
if settings and settings.enabled:
result["enabled"] = True
result["method"] = "frappe.integrations.doctype.ldap_settings.ldap_settings.login"
return result
if self.local_private_key_file:
tls_configuration.private_key_file = self.local_private_key_file
if self.local_server_certificate_file:
tls_configuration.certificate_file = self.local_server_certificate_file
if self.local_ca_certs_file:
tls_configuration.ca_certs_file = self.local_ca_certs_file
server = ldap3.Server(host=self.ldap_server_url, tls=tls_configuration)
bind_type = ldap3.AUTO_BIND_TLS_BEFORE_BIND if self.ssl_tls_mode == "StartTLS" else True
def connect_to_ldap(server_url,
base_dn,
password,
ssl_tls_mode,
trusted_cert,
private_key_file,
server_cert_file,
ca_certs_file):
try:
import ldap3
import ssl
conn = ldap3.Connection(
server=server,
user=base_dn,
password=password,
auto_bind=bind_type,
read_only=True,
raise_exceptions=True)
if trusted_cert == 'Yes':
tls_configuration = ldap3.Tls(validate=ssl.CERT_REQUIRED,
version=ssl.PROTOCOL_TLSv1)
return conn
except ImportError:
msg = _("Please Install the ldap3 library via pip to use ldap functionality.")
frappe.throw(msg, title=_("LDAP Not Installed"))
except ldap3.core.exceptions.LDAPInvalidCredentialsResult:
frappe.throw(_("Invalid username or password"))
except Exception as ex:
frappe.throw(_(str(ex)))
@staticmethod
def get_ldap_client_settings():
# return the settings to be used on the client side.
result = {
"enabled": False
}
ldap = frappe.get_doc("LDAP Settings")
if ldap.enabled:
result["enabled"] = True
result["method"] = "frappe.integrations.doctype.ldap_settings.ldap_settings.login"
return result
@classmethod
def update_user_fields(cls, user, user_data):
updatable_data = {key: value for key, value in user_data.items() if key != 'email'}
for key, value in updatable_data.items():
setattr(user, key, value)
user.save(ignore_permissions=True)
def sync_roles(self, user, additional_groups=None):
current_roles = set([d.role for d in user.get("roles")])
needed_roles = set()
needed_roles.add(self.default_role)
lower_groups = [g.lower() for g in additional_groups or []]
all_mapped_roles = {r.erpnext_role for r in self.ldap_groups}
matched_roles = {r.erpnext_role for r in self.ldap_groups if r.ldap_group.lower() in lower_groups}
unmatched_roles = all_mapped_roles.difference(matched_roles)
needed_roles.update(matched_roles)
roles_to_remove = current_roles.intersection(unmatched_roles)
if not needed_roles.issubset(current_roles):
missing_roles = needed_roles.difference(current_roles)
user.add_roles(*missing_roles)
user.remove_roles(*roles_to_remove)
def create_or_update_user(self, user_data, groups=None):
user = None
if frappe.db.exists("User", user_data['email']):
user = frappe.get_doc("User", user_data['email'])
LDAPSettings.update_user_fields(user=user, user_data=user_data)
else:
tls_configuration = ldap3.Tls(validate=ssl.CERT_NONE,
version=ssl.PROTOCOL_TLSv1)
doc = user_data
doc.update({
"doctype": "User",
"send_welcome_email": 0,
"language": "",
"user_type": "System User",
# "roles": [{
# "role": self.default_role
# }]
})
user = frappe.get_doc(doc)
user.insert(ignore_permissions=True)
# always add default role.
user.add_roles(self.default_role)
if self.ldap_group_field:
self.sync_roles(user, groups)
return user
if private_key_file:
tls_configuration.private_key_file = private_key_file
if server_cert_file:
tls_configuration.certificate_file = server_cert_file
if ca_certs_file:
tls_configuration.ca_certs_file = ca_certs_file
def get_ldap_attributes(self):
ldap_attributes = [self.ldap_email_field, self.ldap_username_field, self.ldap_first_name_field]
server = ldap3.Server(host=server_url,
tls=tls_configuration)
bind_type = ldap3.AUTO_BIND_TLS_BEFORE_BIND if ssl_tls_mode == "StartTLS" else True
if self.ldap_group_field:
ldap_attributes.append(self.ldap_group_field)
conn = ldap3.Connection(server=server,
user=base_dn,
password=password,
auto_bind=bind_type,
read_only=True,
raise_exceptions=True)
if self.ldap_middle_name_field:
ldap_attributes.append(self.ldap_middle_name_field)
return conn
if self.ldap_last_name_field:
ldap_attributes.append(self.ldap_last_name_field)
except ImportError:
msg = _("Please Install the ldap3 library via pip to use ldap functionality.")
frappe.throw(msg, title=_("LDAP Not Installed"))
except ldap3.core.exceptions.LDAPInvalidCredentialsResult:
frappe.throw(_("Invalid Credentials"))
except Exception as ex:
frappe.throw(_(str(ex)))
if self.ldap_phone_field:
ldap_attributes.append(self.ldap_phone_field)
if self.ldap_mobile_field:
ldap_attributes.append(self.ldap_mobile_field)
return ldap_attributes
def authenticate(self, username, password):
if not self.enabled:
frappe.throw(_("LDAP is not enabled."))
user_filter = self.ldap_search_string.format(username)
ldap_attributes = self.get_ldap_attributes()
conn = self.connect_to_ldap(self.base_dn, self.get_password(raise_exception=False))
conn.search(
search_base=self.organizational_unit,
search_filter="({0})".format(user_filter),
attributes=ldap_attributes)
if len(conn.entries) == 1 and conn.entries[0]:
user = conn.entries[0]
# only try and connect as the user, once we have their fqdn entry.
self.connect_to_ldap(base_dn=user.entry_dn, password=password)
groups = None
if self.ldap_group_field:
groups = getattr(user, self.ldap_group_field).values
return self.create_or_update_user(self.convert_ldap_entry_to_dict(user), groups=groups)
else:
frappe.throw(_("Invalid username or password"))
def convert_ldap_entry_to_dict(self, user_entry):
data = {
'username': user_entry[self.ldap_username_field].value,
'email': user_entry[self.ldap_email_field].value,
'first_name': user_entry[self.ldap_first_name_field].value
}
# optional fields
if self.ldap_middle_name_field:
data['middle_name'] = user_entry[self.ldap_middle_name_field].value
if self.ldap_last_name_field:
data['last_name'] = user_entry[self.ldap_last_name_field].value
if self.ldap_phone_field:
data['phone'] = user_entry[self.ldap_phone_field].value
if self.ldap_mobile_field:
data['mobile_no'] = user_entry[self.ldap_mobile_field].value
return data
@frappe.whitelist(allow_guest=True)
def login():
# LDAP LOGIN LOGIC
args = frappe.form_dict
user = authenticate_ldap_user(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd))
ldap = frappe.get_doc("LDAP Settings")
user = ldap.authenticate(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd))
frappe.local.login_manager.user = user.name
frappe.local.login_manager.post_login()
# because of a GET request!
frappe.db.commit()
def authenticate_ldap_user(user=None,
password=None):
params = {}
settings = frappe.get_doc("LDAP Settings")
if settings and settings.enabled:
conn = connect_to_ldap(server_url=settings.ldap_server_url,
base_dn=settings.base_dn,
password=settings.get_password(raise_exception=False),
ssl_tls_mode=settings.ssl_tls_mode,
trusted_cert=settings.require_trusted_certificate,
private_key_file=settings.local_private_key_file,
server_cert_file=settings.local_server_certificate_file,
ca_certs_file=settings.local_ca_certs_file)
user_filter = settings.ldap_search_string.format(user)
conn.search(search_base=settings.organizational_unit,
search_filter="({0})".format(user_filter),
attributes=[settings.ldap_email_field,
settings.ldap_username_field,
settings.ldap_first_name_field])
if len(conn.entries) > 0 and conn.entries[0]:
user = conn.entries[0]
params["email"] = str(user[settings.ldap_email_field])
params["username"] = str(user[settings.ldap_username_field])
params["first_name"] = str(user[settings.ldap_first_name_field])
connect_to_ldap(server_url=settings.ldap_server_url,
base_dn=user.entry_dn,
password=frappe.as_unicode(password),
ssl_tls_mode=settings.ssl_tls_mode,
trusted_cert=settings.require_trusted_certificate,
private_key_file=settings.local_private_key_file,
server_cert_file=settings.local_server_certificate_file,
ca_certs_file=settings.local_ca_certs_file
)
return create_user(params)
else:
frappe.throw(_("Not a valid LDAP user"))
else:
frappe.throw(_("LDAP is not enabled."))
def create_user(params):
if frappe.db.exists("User", params["email"]):
user = frappe.get_doc("User", params["email"])
user.first_name = params["first_name"]
user.username = params["username"]
user.save(ignore_permissions=True)
return user
else:
params.update({
"doctype": "User",
"send_welcome_email": 0,
"language": "",
"user_type": "System User",
"roles": [{
"role": _("Customer")
}]
})
user = frappe.get_doc(params).insert(ignore_permissions=True)
return user

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestLDAPSettings(unittest.TestCase):
pass

View file

@ -66,15 +66,26 @@ def take_backups_if(freq):
@frappe.whitelist()
def take_backups_s3():
def take_backups_s3(retry_count=0):
try:
backup_to_s3()
send_email(True, "S3 Backup Settings")
except JobTimeoutException:
if retry_count < 2:
args = {
"retry_count" :retry_count + 1
}
enqueue("frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3",
queue='long', timeout=1500, **args)
else:
notify()
except Exception:
error_message = frappe.get_traceback()
frappe.errprint(error_message)
send_email(False, "S3 Backup Settings", error_message)
notify()
def notify():
error_message = frappe.get_traceback()
frappe.errprint(error_message)
send_email(False, "S3 Backup Settings", error_message)
def send_email(success, service_name, error_status=None):
if success:
@ -134,6 +145,7 @@ def upload_file_to_s3(filename, folder, conn, bucket):
conn.upload_file(filename, bucket, destpath)
except Exception as e:
frappe.log_error()
print("Error uploading: %s" % (e))

View file

@ -175,10 +175,11 @@ class BaseDocument(object):
if not self.doctype:
return value
if not isinstance(value, BaseDocument):
if "doctype" not in value:
if "doctype" not in value or value['doctype'] is None:
value["doctype"] = self.get_table_field_doctype(key)
if not value["doctype"]:
raise AttributeError(key)
value = get_controller(value["doctype"])(value)
value.init_valid_columns()

View file

@ -477,7 +477,7 @@ def get_field_currency(df, doc=None):
if ":" in cstr(df.get("options")):
split_opts = df.get("options").split(":")
if len(split_opts)==3:
if len(split_opts)==3 and doc.get(split_opts[1]):
currency = frappe.get_cached_value(split_opts[0], doc.get(split_opts[1]), split_opts[2])
else:
currency = doc.get(df.get("options"))

View file

@ -4,6 +4,7 @@
from __future__ import unicode_literals, print_function
from six.moves import range
import frappe
from frappe import _
from frappe.utils import cstr
from frappe.build import html_to_js_template
import re
@ -62,7 +63,7 @@ def render_include(content):
if "{% include" in content:
paths = re.findall(r'''{% include\s['"](.*)['"]\s%}''', content)
if not paths:
frappe.throw('Invalid include path', InvalidIncludePath)
frappe.throw(_('Invalid include path'), InvalidIncludePath)
for path in paths:
app, app_path = path.split('/', 1)

View file

@ -236,7 +236,7 @@ frappe.patches.v12_0.set_primary_key_in_series
execute:frappe.delete_doc("Page", "modules", ignore_missing=True)
frappe.patches.v11_0.set_default_letter_head_source
frappe.patches.v12_0.setup_comments_from_communications
frappe.patches.v12_0.init_desk_settings #11-03-2019
frappe.patches.v12_0.init_desk_settings #16-05-2019
frappe.patches.v12_0.replace_null_values_in_tables
frappe.patches.v12_0.reset_home_settings
frappe.patches.v12_0.update_print_format_type
@ -245,3 +245,4 @@ frappe.patches.v11_0.apply_customization_to_custom_doctype
frappe.patches.v12_0.remove_feedback_rating
frappe.patches.v12_0.move_form_attachments_to_attachments_folder
frappe.patches.v12_0.move_timeline_links_to_dynamic_links
frappe.patches.v12_0.delete_feedback_request_if_exists #1

View file

@ -0,0 +1,8 @@
import frappe
def execute():
frappe.db.sql('''
DELETE from `tabDocType`
WHERE name = 'Feedback Request'
''')

View file

@ -8,4 +8,4 @@ from frappe.desk.moduleview import get_onboard_items
def execute():
"""Reset the initial customizations for desk, with modules, indices and links."""
frappe.reload_doc("core", "doctype", "user")
frappe.db.sql("""update `tabUser` set home_settings = %s""", (''), debug=True)
frappe.db.sql("""update tabUser set home_settings = ''""")

View file

@ -0,0 +1,9 @@
import frappe
def execute():
# convert all /path to path
frappe.db.sql('''
UPDATE `tabWebsite Meta Tag`
SET parent = SUBSTR(parent, 2)
WHERE parent like '/%'
''')

View file

@ -25,7 +25,7 @@ def migrate_style_settings():
website_theme.no_sidebar = cint(frappe.db.get_single_value("Website Settings", "no_sidebar"))
website_theme.save()
website_theme.use_theme()
website_theme.set_as_default()
def map_color_fields(style_settings, website_theme):
color_fields_map = {

View file

@ -19,7 +19,7 @@ class PrintSettings(Document):
try:
import cups
except ImportError:
frappe.throw("You need to install pycups to use this feature!")
frappe.throw(_("You need to install pycups to use this feature!"))
return
try:
cups.setServer(self.server_ip)

View file

@ -36,7 +36,7 @@
"public/js/frappe/ui/messages.js",
"public/js/frappe/translate.js",
"public/js/frappe/utils/pretty_date.js",
"public/js/lib/microtemplate.js",
"public/js/frappe/microtemplate.js",
"public/js/frappe/query_string.js",
"public/js/frappe/ui/dropzone.js",
@ -145,9 +145,7 @@
"public/js/frappe/router_history.js",
"public/js/frappe/defaults.js",
"public/js/frappe/roles_editor.js",
"public/js/lib/microtemplate.js",
"public/js/legacy/handler.js",
"public/js/frappe/microtemplate.js",
"public/js/frappe/ui/page.html",
"public/js/frappe/ui/page.js",
@ -276,6 +274,7 @@
"public/js/frappe/list/list_sidebar.js",
"public/js/frappe/list/list_sidebar.html",
"public/js/frappe/list/list_sidebar_stat.html",
"public/js/frappe/list/list_sidebar_group_by.js",
"public/js/frappe/list/list_view_permission_restrictions.html",
"public/js/frappe/views/gantt/gantt_view.js",
@ -312,7 +311,7 @@
"public/js/frappe/ui/group_by/group_by.js"
],
"js/web_form.min.js": [
"public/js/frappe/misc/datetime.js",
"public/js/frappe/utils/datetime.js",
"public/js/frappe/web_form/webform_script.js",
"public/js/lib/datepicker/datepicker.min.js",
"public/js/lib/datepicker/datepicker.en.js"

Some files were not shown because too many files have changed in this diff Show more