Merge branch 'develop' of github.com:frappe/frappe into multistep_webforms
This commit is contained in:
commit
03c3efdfad
203 changed files with 4635 additions and 2429 deletions
2
.github/helper/documentation.py
vendored
2
.github/helper/documentation.py
vendored
|
|
@ -24,6 +24,8 @@ def docs_link_exists(body):
|
||||||
parts = parsed_url.path.split('/')
|
parts = parsed_url.path.split('/')
|
||||||
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
|
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
|
||||||
return True
|
return True
|
||||||
|
if parsed_url.netloc in ["docs.erpnext.com", "frappeframework.com"]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
|
|
@ -17,7 +17,7 @@ if [ "$TYPE" == "server" ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$DB" == "mariadb" ];then
|
if [ "$DB" == "mariadb" ];then
|
||||||
sudo apt install mariadb-client-10.3
|
sudo apt update && sudo apt install mariadb-client-10.3
|
||||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'";
|
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'";
|
||||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'";
|
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'";
|
||||||
|
|
||||||
|
|
|
||||||
6
.github/workflows/patch-mariadb-tests.yml
vendored
6
.github/workflows/patch-mariadb-tests.yml
vendored
|
|
@ -32,6 +32,12 @@ jobs:
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.9'
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 14
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
- name: Check if build should be run
|
- name: Check if build should be run
|
||||||
id: check-build
|
id: check-build
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -11,6 +11,7 @@ dist/
|
||||||
frappe/docs/current
|
frappe/docs/current
|
||||||
frappe/public/dist
|
frappe/public/dist
|
||||||
.vscode
|
.vscode
|
||||||
|
.vs
|
||||||
node_modules
|
node_modules
|
||||||
.kdev4/
|
.kdev4/
|
||||||
*.kdev4
|
*.kdev4
|
||||||
|
|
|
||||||
24
CODEOWNERS
24
CODEOWNERS
|
|
@ -3,18 +3,18 @@
|
||||||
# These owners will be the default owners for everything in
|
# These owners will be the default owners for everything in
|
||||||
# the repo. Unless a later match takes precedence,
|
# the repo. Unless a later match takes precedence,
|
||||||
|
|
||||||
* @frappe/frappe-review-team
|
* @frappe/frappe-review-team
|
||||||
templates/ @surajshetty3416
|
templates/ @surajshetty3416
|
||||||
www/ @surajshetty3416
|
www/ @surajshetty3416
|
||||||
integrations/ @leela
|
integrations/ @leela
|
||||||
patches/ @surajshetty3416 @gavindsouza
|
patches/ @surajshetty3416 @gavindsouza
|
||||||
email/ @leela
|
email/ @leela
|
||||||
event_streaming/ @ruchamahabal
|
event_streaming/ @ruchamahabal
|
||||||
data_import* @netchampfaris
|
data_import* @netchampfaris
|
||||||
core/ @surajshetty3416
|
core/ @surajshetty3416
|
||||||
database @gavindsouza
|
database @gavindsouza
|
||||||
model @gavindsouza
|
model @gavindsouza
|
||||||
requirements.txt @gavindsouza
|
requirements.txt @gavindsouza
|
||||||
query_builder/ @gavindsouza
|
query_builder/ @gavindsouza
|
||||||
commands/ @gavindsouza
|
commands/ @gavindsouza
|
||||||
workspace @shariquerik
|
workspace @shariquerik
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,15 @@ coverage:
|
||||||
threshold: 0.5%
|
threshold: 0.5%
|
||||||
flags:
|
flags:
|
||||||
- server
|
- server
|
||||||
|
patch:
|
||||||
|
default: false
|
||||||
|
server:
|
||||||
|
target: 85%
|
||||||
|
threshold: 0%
|
||||||
|
only_pulls: true
|
||||||
|
if_ci_failed: ignore
|
||||||
|
flags:
|
||||||
|
- server
|
||||||
|
|
||||||
comment:
|
comment:
|
||||||
layout: "diff, flags"
|
layout: "diff, flags"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ context('Control Rating', () => {
|
||||||
fields: [{
|
fields: [{
|
||||||
'fieldname': 'rate',
|
'fieldname': 'rate',
|
||||||
'fieldtype': 'Rating',
|
'fieldtype': 'Rating',
|
||||||
|
'options': 7
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -40,4 +41,14 @@ context('Control Rating', () => {
|
||||||
.invoke('trigger', 'mouseleave')
|
.invoke('trigger', 'mouseleave')
|
||||||
.should('not.have.class', 'star-hover');
|
.should('not.have.class', 'star-hover');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('check number of stars in rating', () => {
|
||||||
|
get_dialog_with_rating();
|
||||||
|
|
||||||
|
cy.get('div.rating')
|
||||||
|
.first()
|
||||||
|
.children('svg')
|
||||||
|
.should('have.length', 7);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -92,15 +92,15 @@ context('Control Date, Time and DateTime', () => {
|
||||||
date_format: 'dd.mm.yyyy',
|
date_format: 'dd.mm.yyyy',
|
||||||
time_format: 'HH:mm:ss',
|
time_format: 'HH:mm:ss',
|
||||||
value: ' 02.12.2019 11:00:12',
|
value: ' 02.12.2019 11:00:12',
|
||||||
doc_value: '2019-12-02 11:00:12',
|
doc_value: '2019-12-02 00:30:12', // system timezone (America/New_York)
|
||||||
input_value: '02.12.2019 11:00:12'
|
input_value: '02.12.2019 11:00:12' // admin timezone (Asia/Kolkata)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
date_format: 'mm-dd-yyyy',
|
date_format: 'mm-dd-yyyy',
|
||||||
time_format: 'HH:mm',
|
time_format: 'HH:mm',
|
||||||
value: ' 12-02-2019 11:00:00',
|
value: ' 12-02-2019 11:00:00',
|
||||||
doc_value: '2019-12-02 11:00:00',
|
doc_value: '2019-12-02 00:30:00', // system timezone (America/New_York)
|
||||||
input_value: '12-02-2019 11:00'
|
input_value: '12-02-2019 11:00' // admin timezone (Asia/Kolkata)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
datetime_formats.forEach(d => {
|
datetime_formats.forEach(d => {
|
||||||
|
|
|
||||||
49
cypress/integration/grid_keyboard_shortcut.js
Normal file
49
cypress/integration/grid_keyboard_shortcut.js
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
context('Grid Keyboard Shortcut', () => {
|
||||||
|
let total_count = 0;
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.login();
|
||||||
|
cy.visit('/app/doctype/User');
|
||||||
|
});
|
||||||
|
before(() => {
|
||||||
|
cy.login();
|
||||||
|
cy.visit('/app/doctype/User');
|
||||||
|
return cy.window().its('frappe').then(frappe => {
|
||||||
|
frappe.db.count('DocField', {
|
||||||
|
filters: {
|
||||||
|
'parent': 'User', 'parentfield': 'fields', 'parenttype': 'DocType'
|
||||||
|
}
|
||||||
|
}).then((r) => {
|
||||||
|
total_count = r;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Insert new row at the end', () => {
|
||||||
|
cy.add_new_row_in_grid('{ctrl}{shift}{downarrow}', (cy, total_count) => {
|
||||||
|
cy.get('[data-name="new-docfield-1"]').should('have.attr', 'data-idx', `${total_count+1}`);
|
||||||
|
}, total_count);
|
||||||
|
});
|
||||||
|
it('Insert new row at the top', () => {
|
||||||
|
cy.add_new_row_in_grid('{ctrl}{shift}{uparrow}', (cy) => {
|
||||||
|
cy.get('[data-name="new-docfield-1"]').should('have.attr', 'data-idx', '1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Insert new row below', () => {
|
||||||
|
cy.add_new_row_in_grid('{ctrl}{downarrow}', (cy) => {
|
||||||
|
cy.get('[data-name="new-docfield-1"]').should('have.attr', 'data-idx', '2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Insert new row above', () => {
|
||||||
|
cy.add_new_row_in_grid('{ctrl}{uparrow}', (cy) => {
|
||||||
|
cy.get('[data-name="new-docfield-1"]').should('have.attr', 'data-idx', '1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('add_new_row_in_grid', (shortcut_keys, callbackFn, total_count) => {
|
||||||
|
cy.get('.frappe-control[data-fieldname="fields"]').as('table');
|
||||||
|
cy.get('@table').find('.grid-body .col-xs-2').first().click();
|
||||||
|
cy.get('@table').find('.grid-body .col-xs-2')
|
||||||
|
.first().type(shortcut_keys);
|
||||||
|
|
||||||
|
callbackFn(cy, total_count);
|
||||||
|
});
|
||||||
|
|
@ -7,18 +7,13 @@ context('List View', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Keep checkbox checked after Bulk Update', () => {
|
it('Keep checkbox checked after Refresh', () => {
|
||||||
cy.go_to_list('ToDo');
|
cy.go_to_list('ToDo');
|
||||||
cy.get('.list-row-container .list-row-checkbox').click({ multiple: true, force: true });
|
cy.get('.list-row-container .list-row-checkbox').click({ multiple: true, force: true });
|
||||||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click();
|
cy.get('.actions-btn-group button').contains('Actions').should('be.visible');
|
||||||
cy.get('.dropdown-menu li:visible .dropdown-item .menu-item-label[data-label="Edit"]').click();
|
cy.intercept('/api/method/frappe.desk.reportview.get').as('list-refresh');
|
||||||
|
cy.get('button[data-original-title="Refresh"]').click();
|
||||||
cy.get('.modal-body .form-control[data-fieldname="field"]').first().select('Priority').wait(200);
|
cy.wait('@list-refresh');
|
||||||
|
|
||||||
cy.get('.modal-footer .standard-actions .btn-primary').click();
|
|
||||||
cy.wait(500);
|
|
||||||
|
|
||||||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click();
|
|
||||||
cy.get('.list-row-container .list-row-checkbox:checked').should('be.visible');
|
cy.get('.list-row-container .list-row-checkbox:checked').should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,11 @@ from .exceptions import *
|
||||||
from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader)
|
from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader)
|
||||||
from .utils.lazy_loader import lazy_import
|
from .utils.lazy_loader import lazy_import
|
||||||
|
|
||||||
from frappe.query_builder import get_query_builder, patch_query_execute
|
from frappe.query_builder import (
|
||||||
|
get_query_builder,
|
||||||
|
patch_query_execute,
|
||||||
|
patch_query_aggregation,
|
||||||
|
)
|
||||||
|
|
||||||
__version__ = '14.0.0-dev'
|
__version__ = '14.0.0-dev'
|
||||||
|
|
||||||
|
|
@ -211,6 +215,7 @@ def init(site, sites_path=None, new_site=False):
|
||||||
|
|
||||||
setup_module_map()
|
setup_module_map()
|
||||||
patch_query_execute()
|
patch_query_execute()
|
||||||
|
patch_query_aggregation()
|
||||||
|
|
||||||
local.initialised = True
|
local.initialised = True
|
||||||
|
|
||||||
|
|
@ -790,7 +795,9 @@ def has_website_permission(doc=None, ptype='read', user=None, verbose=False, doc
|
||||||
def is_table(doctype):
|
def is_table(doctype):
|
||||||
"""Returns True if `istable` property (indicating child Table) is set for given DocType."""
|
"""Returns True if `istable` property (indicating child Table) is set for given DocType."""
|
||||||
def get_tables():
|
def get_tables():
|
||||||
return db.sql_list("select name from tabDocType where istable=1")
|
return db.get_values(
|
||||||
|
"DocType", filters={"istable": 1}, order_by=None, pluck=True
|
||||||
|
)
|
||||||
|
|
||||||
tables = cache().get_value("is_table", get_tables)
|
tables = cache().get_value("is_table", get_tables)
|
||||||
return doctype in tables
|
return doctype in tables
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,9 @@ def make_form_dict(request):
|
||||||
if 'application/json' in (request.content_type or '') and request_data:
|
if 'application/json' in (request.content_type or '') and request_data:
|
||||||
args = json.loads(request_data)
|
args = json.loads(request_data)
|
||||||
else:
|
else:
|
||||||
args = request.form or request.args
|
args = {}
|
||||||
|
args.update(request.args or {})
|
||||||
|
args.update(request.form or {})
|
||||||
|
|
||||||
if not isinstance(args, dict):
|
if not isinstance(args, dict):
|
||||||
frappe.throw(_("Invalid request arguments"))
|
frappe.throw(_("Invalid request arguments"))
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_p
|
||||||
from frappe.model.base_document import get_controller
|
from frappe.model.base_document import get_controller
|
||||||
from frappe.social.doctype.post.post import frequently_visited_links
|
from frappe.social.doctype.post.post import frequently_visited_links
|
||||||
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo
|
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo
|
||||||
|
from frappe.utils import get_time_zone
|
||||||
|
|
||||||
def get_bootinfo():
|
def get_bootinfo():
|
||||||
"""build and return boot info"""
|
"""build and return boot info"""
|
||||||
|
|
@ -58,6 +59,7 @@ def get_bootinfo():
|
||||||
bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1})
|
bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1})
|
||||||
bootinfo.navbar_settings = get_navbar_settings()
|
bootinfo.navbar_settings = get_navbar_settings()
|
||||||
bootinfo.notification_settings = get_notification_settings()
|
bootinfo.notification_settings = get_notification_settings()
|
||||||
|
set_time_zone(bootinfo)
|
||||||
|
|
||||||
# ipinfo
|
# ipinfo
|
||||||
if frappe.session.data.get('ipinfo'):
|
if frappe.session.data.get('ipinfo'):
|
||||||
|
|
@ -220,8 +222,8 @@ def load_translations(bootinfo):
|
||||||
bootinfo["__messages"] = messages
|
bootinfo["__messages"] = messages
|
||||||
|
|
||||||
def get_user_info():
|
def get_user_info():
|
||||||
user_info = frappe.db.get_all('User', fields=['`name`', 'full_name as fullname', 'user_image as image',
|
user_info = frappe.db.get_all('User', fields=['`name`', 'full_name as fullname', 'user_image as image', 'gender',
|
||||||
'gender', 'email', 'username', 'bio', 'location', 'interest', 'banner_image', 'allowed_in_mentions', 'user_type'],
|
'email', 'username', 'bio', 'location', 'interest', 'banner_image', 'allowed_in_mentions', 'user_type', 'time_zone'],
|
||||||
filters=dict(enabled=1))
|
filters=dict(enabled=1))
|
||||||
|
|
||||||
user_info_map = {d.name: d for d in user_info}
|
user_info_map = {d.name: d for d in user_info}
|
||||||
|
|
@ -324,3 +326,9 @@ def get_desk_settings():
|
||||||
|
|
||||||
def get_notification_settings():
|
def get_notification_settings():
|
||||||
return frappe.get_cached_doc('Notification Settings', frappe.session.user)
|
return frappe.get_cached_doc('Notification Settings', frappe.session.user)
|
||||||
|
|
||||||
|
def set_time_zone(bootinfo):
|
||||||
|
bootinfo.time_zone = {
|
||||||
|
"system": get_time_zone(),
|
||||||
|
"user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None) or get_time_zone()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ Requests via FrappeClient are also handled here.
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_list(doctype, fields=None, filters=None, order_by=None,
|
def get_list(doctype, fields=None, filters=None, order_by=None,
|
||||||
limit_start=None, limit_page_length=20, parent=None, debug=False, as_dict=True):
|
limit_start=None, limit_page_length=20, parent=None, debug=False, as_dict=True, or_filters=None):
|
||||||
'''Returns a list of records by filters, fields, ordering and limit
|
'''Returns a list of records by filters, fields, ordering and limit
|
||||||
|
|
||||||
:param doctype: DocType of the data to be queried
|
:param doctype: DocType of the data to be queried
|
||||||
|
|
@ -34,6 +34,7 @@ def get_list(doctype, fields=None, filters=None, order_by=None,
|
||||||
doctype=doctype,
|
doctype=doctype,
|
||||||
fields=fields,
|
fields=fields,
|
||||||
filters=filters,
|
filters=filters,
|
||||||
|
or_filters=or_filters,
|
||||||
order_by=order_by,
|
order_by=order_by,
|
||||||
limit_start=limit_start,
|
limit_start=limit_start,
|
||||||
limit_page_length=limit_page_length,
|
limit_page_length=limit_page_length,
|
||||||
|
|
|
||||||
|
|
@ -447,11 +447,10 @@ def disable_user(context, email):
|
||||||
@pass_context
|
@pass_context
|
||||||
def migrate(context, skip_failing=False, skip_search_index=False):
|
def migrate(context, skip_failing=False, skip_search_index=False):
|
||||||
"Run patches, sync schema and rebuild files/translations"
|
"Run patches, sync schema and rebuild files/translations"
|
||||||
import re
|
|
||||||
from frappe.migrate import migrate
|
from frappe.migrate import migrate
|
||||||
|
|
||||||
for site in context.sites:
|
for site in context.sites:
|
||||||
print('Migrating', site)
|
click.secho(f"Migrating {site}", fg="green")
|
||||||
frappe.init(site=site)
|
frappe.init(site=site)
|
||||||
frappe.connect()
|
frappe.connect()
|
||||||
try:
|
try:
|
||||||
|
|
@ -697,8 +696,7 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path=
|
||||||
|
|
||||||
drop_user_and_database(frappe.conf.db_name, root_login, root_password)
|
drop_user_and_database(frappe.conf.db_name, root_login, root_password)
|
||||||
|
|
||||||
if not archived_sites_path:
|
archived_sites_path = archived_sites_path or os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived', 'sites')
|
||||||
archived_sites_path = os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived_sites')
|
|
||||||
|
|
||||||
if not os.path.exists(archived_sites_path):
|
if not os.path.exists(archived_sites_path):
|
||||||
os.mkdir(archived_sites_path)
|
os.mkdir(archived_sites_path)
|
||||||
|
|
@ -829,39 +827,37 @@ def publish_realtime(context, event, message, room, user, doctype, docname, afte
|
||||||
@pass_context
|
@pass_context
|
||||||
def browse(context, site, user=None):
|
def browse(context, site, user=None):
|
||||||
'''Opens the site on web browser'''
|
'''Opens the site on web browser'''
|
||||||
from frappe.auth import LoginManager
|
from frappe.auth import CookieManager, LoginManager
|
||||||
from frappe.auth import CookieManager
|
|
||||||
import webbrowser
|
|
||||||
|
|
||||||
site = context.sites[0] if context.sites else site
|
site = get_site(context, raise_err=False) or site
|
||||||
|
|
||||||
if not site:
|
if not site:
|
||||||
click.echo('''Please provide site name\n\nUsage:\n\tbench browse [site-name]\nor\n\tbench --site [site-name] browse''')
|
raise SiteNotSpecifiedError
|
||||||
return
|
|
||||||
|
|
||||||
site = site.lower()
|
if site not in frappe.utils.get_sites():
|
||||||
|
click.echo(f"\nSite named {click.style(site, bold=True)} doesn't exist\n", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
if site in frappe.utils.get_sites():
|
frappe.init(site=site)
|
||||||
frappe.init(site=site)
|
frappe.connect()
|
||||||
frappe.connect()
|
|
||||||
|
|
||||||
sid = ''
|
sid = ''
|
||||||
if user:
|
if user:
|
||||||
if frappe.conf.developer_mode or user == "Administrator":
|
if frappe.conf.developer_mode or user == "Administrator":
|
||||||
frappe.utils.set_request(path="/")
|
frappe.utils.set_request(path="/")
|
||||||
frappe.local.cookie_manager = CookieManager()
|
frappe.local.cookie_manager = CookieManager()
|
||||||
frappe.local.login_manager = LoginManager()
|
frappe.local.login_manager = LoginManager()
|
||||||
frappe.local.login_manager.login_as(user)
|
frappe.local.login_manager.login_as(user)
|
||||||
sid = f'/app?sid={frappe.session.sid}'
|
sid = f'/app?sid={frappe.session.sid}'
|
||||||
else:
|
else:
|
||||||
print("Please enable developer mode to login as a user")
|
click.echo("Please enable developer mode to login as a user")
|
||||||
|
|
||||||
url = f'{frappe.utils.get_site_url(site)}{sid}'
|
url = f'{frappe.utils.get_site_url(site)}{sid}'
|
||||||
if user == "Administrator":
|
|
||||||
print(f'Login URL: {url}')
|
if user == "Administrator":
|
||||||
webbrowser.open(url, new=2)
|
click.echo(f'Login URL: {url}')
|
||||||
else:
|
|
||||||
click.echo("\nSite named \033[1m{}\033[0m doesn't exist\n".format(site))
|
click.launch(url)
|
||||||
|
|
||||||
|
|
||||||
@click.command('start-recording')
|
@click.command('start-recording')
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@
|
||||||
"email_inbox",
|
"email_inbox",
|
||||||
"message_id",
|
"message_id",
|
||||||
"uid",
|
"uid",
|
||||||
|
"imap_folder",
|
||||||
"email_status",
|
"email_status",
|
||||||
"has_attachment",
|
"has_attachment",
|
||||||
"feedback_section",
|
"feedback_section",
|
||||||
|
|
@ -382,12 +383,19 @@
|
||||||
"label": "Timeline Links",
|
"label": "Timeline Links",
|
||||||
"options": "Communication Link",
|
"options": "Communication Link",
|
||||||
"permlevel": 2
|
"permlevel": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "imap_folder",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "IMAP Folder",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-comment",
|
"icon": "fa fa-comment",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-25 09:44:28.963538",
|
"modified": "2021-11-30 09:03:25.728637",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Core",
|
"module": "Core",
|
||||||
"name": "Communication",
|
"name": "Communication",
|
||||||
|
|
|
||||||
|
|
@ -291,6 +291,7 @@ def create_email_account():
|
||||||
"unreplied_for_mins": 20,
|
"unreplied_for_mins": 20,
|
||||||
"send_notification_to": "test_comm@example.com",
|
"send_notification_to": "test_comm@example.com",
|
||||||
"pop3_server": "pop.test.example.com",
|
"pop3_server": "pop.test.example.com",
|
||||||
|
"imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}],
|
||||||
"no_remaining":"0",
|
"no_remaining":"0",
|
||||||
"enable_automatic_linking": 1
|
"enable_automatic_linking": 1
|
||||||
}).insert(ignore_permissions=True)
|
}).insert(ignore_permissions=True)
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,7 @@ class Importer:
|
||||||
new_doc = frappe.new_doc(self.doctype)
|
new_doc = frappe.new_doc(self.doctype)
|
||||||
new_doc.update(doc)
|
new_doc.update(doc)
|
||||||
|
|
||||||
if (meta.autoname or "").lower() != "prompt":
|
if not doc.name and (meta.autoname or "").lower() != "prompt":
|
||||||
# name can only be set directly if autoname is prompt
|
# name can only be set directly if autoname is prompt
|
||||||
new_doc.set("name", None)
|
new_doc.set("name", None)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,6 @@
|
||||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
// MIT License. See license.txt
|
// MIT License. See license.txt
|
||||||
|
|
||||||
// -------------
|
|
||||||
// Menu Display
|
|
||||||
// -------------
|
|
||||||
|
|
||||||
// $(cur_frm.wrapper).on("grid-row-render", function(e, grid_row) {
|
|
||||||
// if(grid_row.doc && grid_row.doc.fieldtype=="Section Break") {
|
|
||||||
// $(grid_row.row).css({"font-weight": "bold"});
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
frappe.ui.form.on('DocType', {
|
frappe.ui.form.on('DocType', {
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
frm.set_query('role', 'permissions', function(doc) {
|
frm.set_query('role', 'permissions', function(doc) {
|
||||||
|
|
@ -129,7 +119,7 @@ frappe.ui.form.on('DocType', {
|
||||||
}
|
}
|
||||||
|
|
||||||
frm.set_df_property('fields', 'reqd', frm.doc.autoname !== 'Prompt');
|
frm.set_df_property('fields', 'reqd', frm.doc.autoname !== 'Prompt');
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.ui.form.on("DocField", {
|
frappe.ui.form.on("DocField", {
|
||||||
|
|
@ -153,11 +143,10 @@ frappe.ui.form.on("DocField", {
|
||||||
curr_value.doctype = doctype;
|
curr_value.doctype = doctype;
|
||||||
curr_value.fieldname = fieldname;
|
curr_value.fieldname = fieldname;
|
||||||
}
|
}
|
||||||
let curr_df_link_doctype = row.fieldtype == "Link" ? row.options : null;
|
|
||||||
|
|
||||||
let doctypes = frm.doc.fields
|
let doctypes = frm.doc.fields
|
||||||
.filter(df => df.fieldtype == "Link")
|
.filter(df => df.fieldtype == "Link")
|
||||||
.filter(df => df.options && df.options != curr_df_link_doctype)
|
.filter(df => df.options && df.fieldname != row.fieldname)
|
||||||
.map(df => ({
|
.map(df => ({
|
||||||
label: `${df.options} (${df.fieldname})`,
|
label: `${df.options} (${df.fieldname})`,
|
||||||
value: df.fieldname
|
value: df.fieldname
|
||||||
|
|
@ -217,5 +206,11 @@ frappe.ui.form.on("DocField", {
|
||||||
$doctype_select.val(curr_value.doctype);
|
$doctype_select.val(curr_value.doctype);
|
||||||
update_fieldname_options();
|
update_fieldname_options();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fieldtype: function(frm) {
|
||||||
|
frm.trigger("max_attachments");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({frm: cur_frm}));
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -75,6 +75,7 @@ class DocType(Document):
|
||||||
self.make_repeatable()
|
self.make_repeatable()
|
||||||
self.validate_nestedset()
|
self.validate_nestedset()
|
||||||
self.validate_website()
|
self.validate_website()
|
||||||
|
self.ensure_minimum_max_attachment_limit()
|
||||||
validate_links_table_fieldnames(self)
|
validate_links_table_fieldnames(self)
|
||||||
|
|
||||||
if not self.is_new():
|
if not self.is_new():
|
||||||
|
|
@ -246,6 +247,22 @@ class DocType(Document):
|
||||||
# clear website cache
|
# clear website cache
|
||||||
clear_cache()
|
clear_cache()
|
||||||
|
|
||||||
|
def ensure_minimum_max_attachment_limit(self):
|
||||||
|
"""Ensure that max_attachments is *at least* bigger than number of attach fields."""
|
||||||
|
from frappe.model import attachment_fieldtypes
|
||||||
|
|
||||||
|
|
||||||
|
if not self.max_attachments:
|
||||||
|
return
|
||||||
|
|
||||||
|
total_attach_fields = len([d for d in self.fields if d.fieldtype in attachment_fieldtypes])
|
||||||
|
if total_attach_fields > self.max_attachments:
|
||||||
|
self.max_attachments = total_attach_fields
|
||||||
|
field_label = frappe.bold(self.meta.get_field("max_attachments").label)
|
||||||
|
frappe.msgprint(_("Number of attachment fields are more than {}, limit updated to {}.")
|
||||||
|
.format(field_label, total_attach_fields),
|
||||||
|
title=_("Insufficient attachment limit"), alert=True)
|
||||||
|
|
||||||
def change_modified_of_parent(self):
|
def change_modified_of_parent(self):
|
||||||
"""Change the timestamp of parent DocType if the current one is a child to clear caches."""
|
"""Change the timestamp of parent DocType if the current one is a child to clear caches."""
|
||||||
if frappe.flags.in_import:
|
if frappe.flags.in_import:
|
||||||
|
|
@ -253,7 +270,7 @@ class DocType(Document):
|
||||||
parent_list = frappe.db.get_all('DocField', 'parent',
|
parent_list = frappe.db.get_all('DocField', 'parent',
|
||||||
dict(fieldtype=['in', frappe.model.table_fields], options=self.name))
|
dict(fieldtype=['in', frappe.model.table_fields], options=self.name))
|
||||||
for p in parent_list:
|
for p in parent_list:
|
||||||
frappe.db.sql('UPDATE `tabDocType` SET modified=%s WHERE `name`=%s', (now(), p.parent))
|
frappe.db.update("DocType", p.parent, {}, for_update=False)
|
||||||
|
|
||||||
def scrub_field_names(self):
|
def scrub_field_names(self):
|
||||||
"""Sluggify fieldnames if not set from Label."""
|
"""Sluggify fieldnames if not set from Label."""
|
||||||
|
|
@ -1057,6 +1074,11 @@ def validate_fields(meta):
|
||||||
if getattr(docfield, 'max_height', None) and (docfield.max_height[-2:] not in ('px', 'em')):
|
if getattr(docfield, 'max_height', None) and (docfield.max_height[-2:] not in ('px', 'em')):
|
||||||
frappe.throw('Max for {} height must be in px, em, rem'.format(frappe.bold(docfield.fieldname)))
|
frappe.throw('Max for {} height must be in px, em, rem'.format(frappe.bold(docfield.fieldname)))
|
||||||
|
|
||||||
|
def check_no_of_ratings(docfield):
|
||||||
|
if docfield.fieldtype == "Rating":
|
||||||
|
if docfield.options and (int(docfield.options) > 10 or int(docfield.options) < 3):
|
||||||
|
frappe.throw(_('Options for Rating field can range from 3 to 10'))
|
||||||
|
|
||||||
fields = meta.get("fields")
|
fields = meta.get("fields")
|
||||||
fieldname_list = [d.fieldname for d in fields]
|
fieldname_list = [d.fieldname for d in fields]
|
||||||
|
|
||||||
|
|
@ -1090,6 +1112,7 @@ def validate_fields(meta):
|
||||||
scrub_fetch_from(d)
|
scrub_fetch_from(d)
|
||||||
validate_data_field_type(d)
|
validate_data_field_type(d)
|
||||||
check_max_height(d)
|
check_max_height(d)
|
||||||
|
check_no_of_ratings(d)
|
||||||
|
|
||||||
check_fold(fields)
|
check_fold(fields)
|
||||||
check_search_fields(meta, fields)
|
check_search_fields(meta, fields)
|
||||||
|
|
|
||||||
0
frappe/core/doctype/doctype_state/__init__.py
Normal file
0
frappe/core/doctype/doctype_state/__init__.py
Normal file
50
frappe/core/doctype/doctype_state/doctype_state.json
Normal file
50
frappe/core/doctype/doctype_state/doctype_state.json
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-08-23 17:21:28.345841",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"title",
|
||||||
|
"color",
|
||||||
|
"custom"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "title",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Title",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Blue",
|
||||||
|
"fieldname": "color",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Color",
|
||||||
|
"options": "Blue\nCyan\nGray\nGreen\nLight Blue\nOrange\nPink\nPurple\nRed\nYellow",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "custom",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Custom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-12-14 14:14:55.716378",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Core",
|
||||||
|
"name": "DocType State",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
8
frappe/core/doctype/doctype_state/doctype_state.py
Normal file
8
frappe/core/doctype/doctype_state/doctype_state.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class DocTypeState(Document):
|
||||||
|
pass
|
||||||
|
|
@ -569,6 +569,24 @@ class File(Document):
|
||||||
frappe.local.rollback_observers.append(self)
|
frappe.local.rollback_observers.append(self)
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def zip_files(files):
|
||||||
|
from six import string_types
|
||||||
|
|
||||||
|
zip_file = io.BytesIO()
|
||||||
|
zf = zipfile.ZipFile(zip_file, "w", zipfile.ZIP_DEFLATED)
|
||||||
|
for _file in files:
|
||||||
|
if isinstance(_file, string_types):
|
||||||
|
_file = frappe.get_doc("File", _file)
|
||||||
|
if not isinstance(_file, File):
|
||||||
|
continue
|
||||||
|
if _file.is_folder:
|
||||||
|
continue
|
||||||
|
zf.writestr(_file.file_name, _file.get_content())
|
||||||
|
zf.close()
|
||||||
|
return zip_file.getvalue()
|
||||||
|
|
||||||
|
|
||||||
def on_doctype_update():
|
def on_doctype_update():
|
||||||
frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"])
|
frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"])
|
||||||
|
|
||||||
|
|
@ -612,6 +630,16 @@ def move_file(file_list, new_parent, old_parent):
|
||||||
frappe.get_doc("File", old_parent).save()
|
frappe.get_doc("File", old_parent).save()
|
||||||
frappe.get_doc("File", new_parent).save()
|
frappe.get_doc("File", new_parent).save()
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def zip_files(files):
|
||||||
|
files = frappe.parse_json(files)
|
||||||
|
zipped_files = File.zip_files(files)
|
||||||
|
frappe.response["filename"] = "files.zip"
|
||||||
|
frappe.response["filecontent"] = zipped_files
|
||||||
|
frappe.response["type"] = "download"
|
||||||
|
|
||||||
|
|
||||||
def setup_folder_path(filename, new_parent):
|
def setup_folder_path(filename, new_parent):
|
||||||
file = frappe.get_doc("File", filename)
|
file = frappe.get_doc("File", filename)
|
||||||
file.folder = new_parent
|
file.folder = new_parent
|
||||||
|
|
@ -940,20 +968,14 @@ def get_files_by_search_text(text):
|
||||||
|
|
||||||
def update_existing_file_docs(doc):
|
def update_existing_file_docs(doc):
|
||||||
# Update is private and file url of all file docs that point to the same file
|
# Update is private and file url of all file docs that point to the same file
|
||||||
frappe.db.sql("""
|
file_doctype = frappe.qb.DocType("File")
|
||||||
UPDATE `tabFile`
|
(
|
||||||
SET
|
frappe.qb.update(file_doctype)
|
||||||
file_url = %(file_url)s,
|
.set(file_doctype.file_url, doc.file_url)
|
||||||
is_private = %(is_private)s
|
.set(file_doctype.is_private, doc.is_private)
|
||||||
WHERE
|
.where(file_doctype.content_hash == doc.content_hash)
|
||||||
content_hash = %(content_hash)s
|
.where(file_doctype.name != doc.name)
|
||||||
and name != %(file_name)s
|
).run()
|
||||||
""", dict(
|
|
||||||
file_url=doc.file_url,
|
|
||||||
is_private=doc.is_private,
|
|
||||||
content_hash=doc.content_hash,
|
|
||||||
file_name=doc.name
|
|
||||||
))
|
|
||||||
|
|
||||||
def attach_files_to_document(doc, event):
|
def attach_files_to_document(doc, event):
|
||||||
""" Runs on on_update hook of all documents.
|
""" Runs on on_update hook of all documents.
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,23 @@
|
||||||
// Copyright (c) 2020, Frappe Technologies and contributors
|
// Copyright (c) 2020, Frappe Technologies and contributors
|
||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Module Profile', {
|
frappe.ui.form.on("Module Profile", {
|
||||||
refresh: function(frm) {
|
refresh: function (frm) {
|
||||||
if (has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
|
if (has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
|
||||||
if (!frm.module_editor && frm.doc.__onload && frm.doc.__onload.all_modules) {
|
if (!frm.module_editor && frm.doc.__onload && frm.doc.__onload.all_modules) {
|
||||||
let module_area = $('<div style="min-height: 300px">')
|
const module_area = $(frm.fields_dict.module_html.wrapper);
|
||||||
.appendTo(frm.fields_dict.module_html.wrapper);
|
|
||||||
|
|
||||||
frm.module_editor = new frappe.ModuleEditor(frm, module_area);
|
frm.module_editor = new frappe.ModuleEditor(frm, module_area);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frm.module_editor) {
|
if (frm.module_editor) {
|
||||||
frm.module_editor.refresh();
|
frm.module_editor.show();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
validate: function (frm) {
|
||||||
|
if (frm.module_editor) {
|
||||||
|
frm.module_editor.set_modules_in_table();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,17 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [
|
||||||
"modified": "2021-01-03 15:36:52.622696",
|
{
|
||||||
|
"link_doctype": "User",
|
||||||
|
"link_fieldname": "module_profile"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modified": "2021-12-03 15:47:21.296443",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Core",
|
"module": "Core",
|
||||||
"name": "Module Profile",
|
"name": "Module Profile",
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,175 +1,80 @@
|
||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_guest_to_view": 0,
|
"autoname": "role_profile",
|
||||||
"allow_import": 0,
|
"creation": "2017-08-31 04:16:38.764465",
|
||||||
"allow_rename": 0,
|
"doctype": "DocType",
|
||||||
"autoname": "role_profile",
|
"editable_grid": 1,
|
||||||
"beta": 0,
|
"engine": "InnoDB",
|
||||||
"creation": "2017-08-31 04:16:38.764465",
|
"field_order": [
|
||||||
"custom": 0,
|
"role_profile",
|
||||||
"docstatus": 0,
|
"roles_html",
|
||||||
"doctype": "DocType",
|
"roles"
|
||||||
"document_type": "",
|
],
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"fieldname": "role_profile",
|
||||||
"allow_on_submit": 0,
|
"fieldtype": "Data",
|
||||||
"bold": 0,
|
"in_list_view": 1,
|
||||||
"collapsible": 0,
|
"label": "Role Name",
|
||||||
"columns": 0,
|
"reqd": 1,
|
||||||
"fieldname": "role_profile",
|
|
||||||
"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": "Role Name",
|
|
||||||
"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": 1
|
"unique": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"fieldname": "roles_html",
|
||||||
"allow_on_submit": 0,
|
"fieldtype": "HTML",
|
||||||
"bold": 0,
|
"label": "Roles HTML",
|
||||||
"collapsible": 0,
|
"read_only": 1
|
||||||
"columns": 0,
|
},
|
||||||
"fieldname": "roles_html",
|
|
||||||
"fieldtype": "HTML",
|
|
||||||
"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": "Roles HTML",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"fieldname": "roles",
|
||||||
"allow_on_submit": 0,
|
"fieldtype": "Table",
|
||||||
"bold": 0,
|
"hidden": 1,
|
||||||
"collapsible": 0,
|
"label": "Roles Assigned",
|
||||||
"columns": 0,
|
"options": "Has Role",
|
||||||
"fieldname": "roles",
|
"permlevel": 1,
|
||||||
"fieldtype": "Table",
|
"print_hide": 1,
|
||||||
"hidden": 1,
|
"read_only": 1
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Roles Assigned",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Has Role",
|
|
||||||
"permlevel": 1,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 1,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
"links": [
|
||||||
"hide_heading": 0,
|
{
|
||||||
"hide_toolbar": 0,
|
"link_doctype": "User",
|
||||||
"idx": 0,
|
"link_fieldname": "role_profile_name"
|
||||||
"image_view": 0,
|
}
|
||||||
"in_create": 0,
|
],
|
||||||
"is_submittable": 0,
|
"modified": "2021-12-03 15:45:45.270963",
|
||||||
"issingle": 0,
|
"modified_by": "Administrator",
|
||||||
"istable": 0,
|
"module": "Core",
|
||||||
"max_attachments": 0,
|
"name": "Role Profile",
|
||||||
"modified": "2017-10-17 11:05:11.183066",
|
"naming_rule": "Expression (old style)",
|
||||||
"modified_by": "Administrator",
|
"owner": "Administrator",
|
||||||
"module": "Core",
|
|
||||||
"name": "Role Profile",
|
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"amend": 0,
|
"create": 1,
|
||||||
"apply_user_permissions": 0,
|
"delete": 1,
|
||||||
"cancel": 0,
|
"email": 1,
|
||||||
"create": 1,
|
"export": 1,
|
||||||
"delete": 1,
|
"print": 1,
|
||||||
"email": 1,
|
"read": 1,
|
||||||
"export": 1,
|
"report": 1,
|
||||||
"if_owner": 0,
|
"role": "System Manager",
|
||||||
"import": 0,
|
"share": 1,
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "System Manager",
|
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amend": 0,
|
"email": 1,
|
||||||
"apply_user_permissions": 0,
|
"export": 1,
|
||||||
"cancel": 0,
|
"permlevel": 1,
|
||||||
"create": 0,
|
"print": 1,
|
||||||
"delete": 0,
|
"read": 1,
|
||||||
"email": 1,
|
"report": 1,
|
||||||
"export": 1,
|
"role": "System Manager",
|
||||||
"if_owner": 0,
|
"share": 1,
|
||||||
"import": 0,
|
|
||||||
"permlevel": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "System Manager",
|
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 0,
|
"sort_field": "modified",
|
||||||
"read_only": 0,
|
"sort_order": "DESC",
|
||||||
"read_only_onload": 0,
|
"title_field": "role_profile",
|
||||||
"show_name_in_global_search": 0,
|
"track_changes": 1
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"title_field": "role_profile",
|
|
||||||
"track_changes": 1,
|
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
||||||
|
|
@ -10,7 +10,7 @@ from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
|
||||||
class TestScheduledJobType(unittest.TestCase):
|
class TestScheduledJobType(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
frappe.db.sql('truncate `tabScheduled Job Type`')
|
frappe.db.truncate("Scheduled Job Type")
|
||||||
sync_jobs()
|
sync_jobs()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,19 @@ def run_server_script_for_doc_event(doc, event):
|
||||||
if scripts:
|
if scripts:
|
||||||
# run all scripts for this doctype + event
|
# run all scripts for this doctype + event
|
||||||
for script_name in scripts:
|
for script_name in scripts:
|
||||||
frappe.get_doc('Server Script', script_name).execute_doc(doc)
|
try:
|
||||||
|
frappe.get_doc('Server Script', script_name).execute_doc(doc)
|
||||||
|
except Exception as e:
|
||||||
|
message = frappe._('Error executing Server Script {0}. Open Browser Console to see traceback.').format(
|
||||||
|
frappe.utils.get_link_to_form('Server Script', script_name)
|
||||||
|
)
|
||||||
|
exception = type(e)
|
||||||
|
if getattr(frappe, 'request', None):
|
||||||
|
# all exceptions throw 500 which is internal server error
|
||||||
|
# however server script error is a user error
|
||||||
|
# so we should throw 417 which is expectation failed
|
||||||
|
exception.http_status_code = 417
|
||||||
|
frappe.throw(title=frappe._('Server Script Error'), msg=message, exc=exception)
|
||||||
|
|
||||||
def get_server_script_map():
|
def get_server_script_map():
|
||||||
# fetch cached server script methods
|
# fetch cached server script methods
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ class TestServerScript(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
frappe.db.sql('truncate `tabServer Script`')
|
frappe.db.truncate("Server Script")
|
||||||
frappe.get_doc('User', 'Administrator').add_roles('Script Manager')
|
frappe.get_doc('User', 'Administrator').add_roles('Script Manager')
|
||||||
for script in scripts:
|
for script in scripts:
|
||||||
script_doc = frappe.get_doc(doctype ='Server Script')
|
script_doc = frappe.get_doc(doctype ='Server Script')
|
||||||
|
|
@ -88,7 +88,7 @@ class TestServerScript(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
frappe.db.sql('truncate `tabServer Script`')
|
frappe.db.truncate("Server Script")
|
||||||
frappe.cache().delete_value('server_script_map')
|
frappe.cache().delete_value('server_script_map')
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
||||||
|
|
@ -32,5 +32,11 @@ frappe.ui.form.on("System Settings", {
|
||||||
frm.set_value('prepared_report_expiry_period', 7);
|
frm.set_value('prepared_report_expiry_period', 7);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
on_update: function(frm) {
|
||||||
|
if (frappe.boot.time_zone && frappe.boot.time_zone.system !== frm.doc.time_zone) {
|
||||||
|
// Clear cache after saving to refresh the values of boot.
|
||||||
|
frappe.ui.toolbar.clear_cache();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,9 @@
|
||||||
"attach_view_link",
|
"attach_view_link",
|
||||||
"prepared_report_section",
|
"prepared_report_section",
|
||||||
"enable_prepared_report_auto_deletion",
|
"enable_prepared_report_auto_deletion",
|
||||||
"prepared_report_expiry_period"
|
"prepared_report_expiry_period",
|
||||||
|
"system_updates_section",
|
||||||
|
"disable_system_update_notification"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
|
|
@ -95,6 +97,7 @@
|
||||||
"fieldname": "time_zone",
|
"fieldname": "time_zone",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Time Zone",
|
"label": "Time Zone",
|
||||||
|
"read_only": 1,
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -462,12 +465,24 @@
|
||||||
"fieldname": "encrypt_backup",
|
"fieldname": "encrypt_backup",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Encrypt Backups"
|
"label": "Encrypt Backups"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "system_updates_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "System Updates"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "disable_system_update_notification",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Disable System Update Notification"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-10-21 19:24:15.232430",
|
"modified": "2021-11-29 18:09:53.601629",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Core",
|
"module": "Core",
|
||||||
"name": "System Settings",
|
"name": "System Settings",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from frappe.model.document import Document
|
||||||
from frappe.query_builder import DocType
|
from frappe.query_builder import DocType
|
||||||
from frappe.utils import cint, now_datetime
|
from frappe.utils import cint, now_datetime
|
||||||
|
|
||||||
|
|
||||||
class TransactionLog(Document):
|
class TransactionLog(Document):
|
||||||
def before_insert(self):
|
def before_insert(self):
|
||||||
index = get_current_index()
|
index = get_current_index()
|
||||||
|
|
@ -29,18 +30,15 @@ class TransactionLog(Document):
|
||||||
def hash_line(self):
|
def hash_line(self):
|
||||||
sha = hashlib.sha256()
|
sha = hashlib.sha256()
|
||||||
sha.update(
|
sha.update(
|
||||||
frappe.safe_encode(str(self.row_index)) + \
|
frappe.safe_encode(str(self.row_index))
|
||||||
frappe.safe_encode(str(self.timestamp)) + \
|
+ frappe.safe_encode(str(self.timestamp))
|
||||||
frappe.safe_encode(str(self.data))
|
+ frappe.safe_encode(str(self.data))
|
||||||
)
|
)
|
||||||
return sha.hexdigest()
|
return sha.hexdigest()
|
||||||
|
|
||||||
def hash_chain(self):
|
def hash_chain(self):
|
||||||
sha = hashlib.sha256()
|
sha = hashlib.sha256()
|
||||||
sha.update(
|
sha.update(frappe.safe_encode(str(self.transaction_hash)) + frappe.safe_encode(str(self.previous_hash)))
|
||||||
frappe.safe_encode(str(self.transaction_hash)) + \
|
|
||||||
frappe.safe_encode(str(self.previous_hash))
|
|
||||||
)
|
|
||||||
return sha.hexdigest()
|
return sha.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -251,7 +251,7 @@ class TestUser(unittest.TestCase):
|
||||||
c = FrappeClient(url)
|
c = FrappeClient(url)
|
||||||
res1 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
|
res1 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
|
||||||
res2 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
|
res2 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
|
||||||
self.assertEqual(res1.status_code, 200)
|
self.assertEqual(res1.status_code, 400)
|
||||||
self.assertEqual(res2.status_code, 417)
|
self.assertEqual(res2.status_code, 417)
|
||||||
|
|
||||||
def test_user_rename(self):
|
def test_user_rename(self):
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ frappe.ui.form.on('User', {
|
||||||
let d = frm.add_child("block_modules");
|
let d = frm.add_child("block_modules");
|
||||||
d.module = v.module;
|
d.module = v.module;
|
||||||
});
|
});
|
||||||
frm.module_editor && frm.module_editor.refresh();
|
frm.module_editor && frm.module_editor.show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +77,12 @@ frappe.ui.form.on('User', {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
var doc = frm.doc;
|
let doc = frm.doc;
|
||||||
|
|
||||||
|
if (frm.is_new()) {
|
||||||
|
frm.set_value("time_zone", frappe.sys_defaults.time_zone);
|
||||||
|
}
|
||||||
|
|
||||||
if (in_list(['System User', 'Website User'], frm.doc.user_type)
|
if (in_list(['System User', 'Website User'], frm.doc.user_type)
|
||||||
&& !frm.is_new() && !frm.roles_editor && frm.can_edit_roles) {
|
&& !frm.is_new() && !frm.roles_editor && frm.can_edit_roles) {
|
||||||
frm.reload_doc();
|
frm.reload_doc();
|
||||||
|
|
@ -180,7 +185,7 @@ frappe.ui.form.on('User', {
|
||||||
frm.roles_editor.show();
|
frm.roles_editor.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
frm.module_editor && frm.module_editor.refresh();
|
frm.module_editor && frm.module_editor.show();
|
||||||
|
|
||||||
if(frappe.session.user==doc.name) {
|
if(frappe.session.user==doc.name) {
|
||||||
// update display settings
|
// update display settings
|
||||||
|
|
@ -267,6 +272,12 @@ frappe.ui.form.on('User', {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
on_update: function(frm) {
|
||||||
|
if (frappe.boot.time_zone && frappe.boot.time_zone.user !== frm.doc.time_zone) {
|
||||||
|
// Clear cache after saving to refresh the values of boot.
|
||||||
|
frappe.ui.toolbar.clear_cache();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import frappe.defaults
|
||||||
import frappe.permissions
|
import frappe.permissions
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import (cint, flt, has_gravatar, escape_html, format_datetime,
|
from frappe.utils import (cint, flt, has_gravatar, escape_html, format_datetime,
|
||||||
now_datetime, get_formatted_email, today)
|
now_datetime, get_formatted_email, today, get_time_zone)
|
||||||
from frappe import throw, msgprint, _
|
from frappe import throw, msgprint, _
|
||||||
from frappe.utils.password import update_password as _update_password, check_password, get_password_reset_limit
|
from frappe.utils.password import update_password as _update_password, check_password, get_password_reset_limit
|
||||||
from frappe.desk.notifications import clear_notifications
|
from frappe.desk.notifications import clear_notifications
|
||||||
|
|
@ -74,6 +74,7 @@ class User(Document):
|
||||||
self.validate_roles()
|
self.validate_roles()
|
||||||
self.validate_allowed_modules()
|
self.validate_allowed_modules()
|
||||||
self.validate_user_image()
|
self.validate_user_image()
|
||||||
|
self.set_time_zone()
|
||||||
|
|
||||||
if self.language == "Loading...":
|
if self.language == "Loading...":
|
||||||
self.language = None
|
self.language = None
|
||||||
|
|
@ -213,15 +214,12 @@ class User(Document):
|
||||||
user_type_doc.update_modules_in_user(self)
|
user_type_doc.update_modules_in_user(self)
|
||||||
|
|
||||||
def has_desk_access(self):
|
def has_desk_access(self):
|
||||||
'''Return true if any of the set roles has desk access'''
|
"""Return true if any of the set roles has desk access"""
|
||||||
if not self.roles:
|
if not self.roles:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return len(frappe.db.sql("""select name
|
role_table = DocType("Role")
|
||||||
from `tabRole` where desk_access=1
|
return frappe.db.count(role_table, ((role_table.desk_access == 1) & (role_table.name.isin([d.role for d in self.roles]))))
|
||||||
and name in ({0}) limit 1""".format(', '.join(['%s'] * len(self.roles))),
|
|
||||||
[d.role for d in self.roles]))
|
|
||||||
|
|
||||||
|
|
||||||
def share_with_self(self):
|
def share_with_self(self):
|
||||||
frappe.share.add(self.doctype, self.name, self.name, write=1, share=1,
|
frappe.share.add(self.doctype, self.name, self.name, write=1, share=1,
|
||||||
|
|
@ -230,11 +228,11 @@ class User(Document):
|
||||||
def validate_share(self, docshare):
|
def validate_share(self, docshare):
|
||||||
pass
|
pass
|
||||||
# if docshare.user == self.name:
|
# if docshare.user == self.name:
|
||||||
# if self.user_type=="System User":
|
# if self.user_type=="System User":
|
||||||
# if docshare.share != 1:
|
# if docshare.share != 1:
|
||||||
# frappe.throw(_("Sorry! User should have complete access to their own record."))
|
# frappe.throw(_("Sorry! User should have complete access to their own record."))
|
||||||
# else:
|
# else:
|
||||||
# frappe.throw(_("Sorry! Sharing with Website User is prohibited."))
|
# frappe.throw(_("Sorry! Sharing with Website User is prohibited."))
|
||||||
|
|
||||||
def send_password_notification(self, new_password):
|
def send_password_notification(self, new_password):
|
||||||
try:
|
try:
|
||||||
|
|
@ -279,12 +277,20 @@ class User(Document):
|
||||||
return link
|
return link
|
||||||
|
|
||||||
def get_other_system_managers(self):
|
def get_other_system_managers(self):
|
||||||
return frappe.db.sql("""select distinct `user`.`name` from `tabHas Role` as `user_role`, `tabUser` as `user`
|
user_doctype = DocType("User").as_("user")
|
||||||
where user_role.role='System Manager'
|
user_role_doctype = DocType("Has Role").as_("user_role")
|
||||||
and `user`.docstatus<2
|
return (
|
||||||
and `user`.enabled=1
|
frappe.qb.from_(user_doctype)
|
||||||
and `user_role`.parent = `user`.name
|
.from_(user_role_doctype)
|
||||||
and `user_role`.parent not in ('Administrator', %s) limit 1""", (self.name,))
|
.select(user_doctype.name)
|
||||||
|
.where(user_role_doctype.role == 'System Manager')
|
||||||
|
.where(user_doctype.docstatus < 2)
|
||||||
|
.where(user_doctype.enabled == 1)
|
||||||
|
.where(user_role_doctype.parent == user_doctype.name)
|
||||||
|
.where(user_role_doctype.parent.notin(["Administrator", self.name]))
|
||||||
|
.limit(1)
|
||||||
|
.distinct()
|
||||||
|
).run()
|
||||||
|
|
||||||
def get_fullname(self):
|
def get_fullname(self):
|
||||||
"""get first_name space last_name"""
|
"""get first_name space last_name"""
|
||||||
|
|
@ -358,8 +364,12 @@ class User(Document):
|
||||||
|
|
||||||
# delete todos
|
# delete todos
|
||||||
frappe.db.delete("ToDo", {"owner": self.name})
|
frappe.db.delete("ToDo", {"owner": self.name})
|
||||||
frappe.db.sql("""UPDATE `tabToDo` SET `assigned_by`=NULL WHERE `assigned_by`=%s""",
|
todo_table = DocType("ToDo")
|
||||||
(self.name,))
|
(
|
||||||
|
frappe.qb.update(todo_table)
|
||||||
|
.set(todo_table.assigned_by, None)
|
||||||
|
.where(todo_table.assigned_by == self.name)
|
||||||
|
).run()
|
||||||
|
|
||||||
# delete events
|
# delete events
|
||||||
frappe.db.delete("Event", {"owner": self.name, "event_type": "Private"})
|
frappe.db.delete("Event", {"owner": self.name, "event_type": "Private"})
|
||||||
|
|
@ -425,10 +435,7 @@ class User(Document):
|
||||||
frappe.rename_doc("Notification Settings", old_name, new_name, force=True, show_alert=False)
|
frappe.rename_doc("Notification Settings", old_name, new_name, force=True, show_alert=False)
|
||||||
|
|
||||||
# set email
|
# set email
|
||||||
table = DocType("User")
|
frappe.db.update("User", new_name, "email", new_name)
|
||||||
frappe.qb.update(table).where(
|
|
||||||
table.name == new_name
|
|
||||||
).set("email", new_name).run()
|
|
||||||
|
|
||||||
def append_roles(self, *roles):
|
def append_roles(self, *roles):
|
||||||
"""Add roles to user"""
|
"""Add roles to user"""
|
||||||
|
|
@ -590,6 +597,10 @@ class User(Document):
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
def set_time_zone(self):
|
||||||
|
if not self.time_zone:
|
||||||
|
self.time_zone = get_time_zone()
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_timezones():
|
def get_timezones():
|
||||||
import pytz
|
import pytz
|
||||||
|
|
@ -698,28 +709,19 @@ def has_email_account(email):
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=False)
|
@frappe.whitelist(allow_guest=False)
|
||||||
def get_email_awaiting(user):
|
def get_email_awaiting(user):
|
||||||
waiting = frappe.db.sql("""select email_account,email_id
|
waiting = frappe.get_all("User Email", fields=["email_account", "email_id"], filters={"awaiting_password": 1, "parent": user})
|
||||||
from `tabUser Email`
|
|
||||||
where awaiting_password = 1
|
|
||||||
and parent = %(user)s""", {"user":user}, as_dict=1)
|
|
||||||
if waiting:
|
if waiting:
|
||||||
return waiting
|
return waiting
|
||||||
else:
|
else:
|
||||||
frappe.db.sql("""update `tabUser Email`
|
user_email_table = DocType("User Email")
|
||||||
set awaiting_password =0
|
frappe.qb.update(user_email_table).set(user_email_table.user_email_table, 0).where(user_email_table.parent == user).run()
|
||||||
where parent = %(user)s""",{"user":user})
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def ask_pass_update():
|
def ask_pass_update():
|
||||||
# update the sys defaults as to awaiting users
|
# update the sys defaults as to awaiting users
|
||||||
from frappe.utils import set_default
|
from frappe.utils import set_default
|
||||||
|
|
||||||
doctype = DocType("User Email")
|
password_list = frappe.get_all("User Email", filters={"awaiting_password": True}, pluck="parent", distinct=True)
|
||||||
users = frappe.qb.from_(doctype).where(doctype.awaiting_password == 1).select(
|
|
||||||
doctype.parent.as_("user")
|
|
||||||
).distinct().run(as_dict=True)
|
|
||||||
|
|
||||||
password_list = [ user.get("user") for user in users ]
|
|
||||||
set_default("email_user_password", u','.join(password_list))
|
set_default("email_user_password", u','.join(password_list))
|
||||||
|
|
||||||
def _get_user_for_update_password(key, old_password):
|
def _get_user_for_update_password(key, old_password):
|
||||||
|
|
@ -811,6 +813,7 @@ def reset_password(user):
|
||||||
return frappe.msgprint(_("Password reset instructions have been sent to your email"))
|
return frappe.msgprint(_("Password reset instructions have been sent to your email"))
|
||||||
|
|
||||||
except frappe.DoesNotExistError:
|
except frappe.DoesNotExistError:
|
||||||
|
frappe.local.response['http_status_code'] = 400
|
||||||
frappe.clear_messages()
|
frappe.clear_messages()
|
||||||
return 'not found'
|
return 'not found'
|
||||||
|
|
||||||
|
|
@ -887,8 +890,7 @@ def get_active_users():
|
||||||
|
|
||||||
def get_website_users():
|
def get_website_users():
|
||||||
"""Returns total no. of website users"""
|
"""Returns total no. of website users"""
|
||||||
return frappe.db.sql("""select count(*) from `tabUser`
|
return frappe.db.count("User", filters={"enabled": True, "user_type": "Website User"})
|
||||||
where enabled = 1 and user_type = 'Website User'""")[0][0]
|
|
||||||
|
|
||||||
def get_active_website_users():
|
def get_active_website_users():
|
||||||
"""Returns No. of website users who logged in, in the last 3 days"""
|
"""Returns No. of website users who logged in, in the last 3 days"""
|
||||||
|
|
|
||||||
56
frappe/core/form_tour/doctype/doctype.json
Normal file
56
frappe/core/form_tour/doctype/doctype.json
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
"creation": "2021-11-23 12:38:52.807353",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Form Tour",
|
||||||
|
"first_document": 0,
|
||||||
|
"idx": 0,
|
||||||
|
"include_name_field": 1,
|
||||||
|
"is_standard": 1,
|
||||||
|
"modified": "2021-11-25 17:03:01.646360",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Core",
|
||||||
|
"name": "Doctype",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"reference_doctype": "DocType",
|
||||||
|
"save_on_complete": 1,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"description": "Select a Module to which this DocType would belong",
|
||||||
|
"field": "",
|
||||||
|
"fieldname": "module",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"has_next_condition": 0,
|
||||||
|
"is_table_field": 0,
|
||||||
|
"label": "Module",
|
||||||
|
"parent_field": "",
|
||||||
|
"position": "Right",
|
||||||
|
"title": "Module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Check this to make the DocType as Custom",
|
||||||
|
"field": "",
|
||||||
|
"fieldname": "custom",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"has_next_condition": 1,
|
||||||
|
"is_table_field": 0,
|
||||||
|
"label": "Custom?",
|
||||||
|
"next_step_condition": "eval: doc.custom",
|
||||||
|
"parent_field": "",
|
||||||
|
"position": "Left",
|
||||||
|
"title": "Custom "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "A Field (or a docfield) defines a property of a DocType. You can define the column name, label, datatype and more for DocFields. For instance, a ToDo doctype has fields description, status and priority. These ultimately become columns in the database table tabToDo.",
|
||||||
|
"field": "",
|
||||||
|
"fieldname": "fields",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"has_next_condition": 0,
|
||||||
|
"is_table_field": 0,
|
||||||
|
"label": "Fields",
|
||||||
|
"parent_field": "",
|
||||||
|
"position": "Top",
|
||||||
|
"title": "Fields"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Doctype"
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
# License: MIT. See LICENSE
|
# License: MIT. See LICENSE
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.query_builder import DocType, Interval
|
||||||
|
from frappe.query_builder.functions import Now
|
||||||
|
|
||||||
|
|
||||||
def get_notification_config():
|
def get_notification_config():
|
||||||
return {
|
return {
|
||||||
|
|
@ -39,28 +42,40 @@ def get_todays_events(as_list=False):
|
||||||
|
|
||||||
def get_unseen_likes():
|
def get_unseen_likes():
|
||||||
"""Returns count of unseen likes"""
|
"""Returns count of unseen likes"""
|
||||||
return frappe.db.sql("""select count(*) from `tabComment`
|
|
||||||
where
|
comment_doctype = DocType("Comment")
|
||||||
comment_type='Like'
|
return frappe.db.count(comment_doctype,
|
||||||
and modified >= (NOW() - INTERVAL '1' YEAR)
|
filters=(
|
||||||
and owner is not null and owner!=%(user)s
|
(comment_doctype.comment_type == "Like")
|
||||||
and reference_owner=%(user)s
|
& (comment_doctype.modified >= Now() - Interval(years=1))
|
||||||
and seen=0
|
& (comment_doctype.owner.notnull())
|
||||||
""", {"user": frappe.session.user})[0][0]
|
& (comment_doctype.owner != frappe.session.user)
|
||||||
|
& (comment_doctype.reference_owner == frappe.session.user)
|
||||||
|
& (comment_doctype.seen == 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_unread_emails():
|
def get_unread_emails():
|
||||||
"returns unread emails for a user"
|
"returns count of unread emails for a user"
|
||||||
|
|
||||||
return frappe.db.sql("""\
|
communication_doctype = DocType("Communication")
|
||||||
SELECT count(*)
|
user_doctype = DocType("User")
|
||||||
FROM `tabCommunication`
|
distinct_email_accounts = (
|
||||||
WHERE communication_type='Communication'
|
frappe.qb.from_(user_doctype)
|
||||||
AND communication_medium='Email'
|
.select(user_doctype.email_account)
|
||||||
AND sent_or_received='Received'
|
.where(user_doctype.parent == frappe.session.user)
|
||||||
AND email_status not in ('Spam', 'Trash')
|
.distinct()
|
||||||
AND email_account in (
|
)
|
||||||
SELECT distinct email_account from `tabUser Email` WHERE parent=%(user)s
|
|
||||||
|
return frappe.db.count(communication_doctype,
|
||||||
|
filters=(
|
||||||
|
(communication_doctype.communication_type == "Communication")
|
||||||
|
& (communication_doctype.communication_medium == "Email")
|
||||||
|
& (communication_doctype.sent_or_received == "Received")
|
||||||
|
& (communication_doctype.email_status.notin(["spam", "Trash"]))
|
||||||
|
& (communication_doctype.email_account.isin(distinct_email_accounts))
|
||||||
|
& (communication_doctype.modified >= Now() - Interval(years=1))
|
||||||
|
& (communication_doctype.seen == 0)
|
||||||
)
|
)
|
||||||
AND modified >= (NOW() - INTERVAL '1' YEAR)
|
)
|
||||||
AND seen=0
|
|
||||||
""", {"user": frappe.session.user})[0][0]
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.model.docfield import supports_translation
|
from frappe.model.docfield import supports_translation
|
||||||
from frappe.model import core_doctypes_list
|
from frappe.model import core_doctypes_list
|
||||||
|
from frappe.query_builder.functions import IfNull
|
||||||
|
|
||||||
class CustomField(Document):
|
class CustomField(Document):
|
||||||
def autoname(self):
|
def autoname(self):
|
||||||
|
|
@ -115,9 +116,7 @@ def get_fields_label(doctype=None):
|
||||||
def create_custom_field_if_values_exist(doctype, df):
|
def create_custom_field_if_values_exist(doctype, df):
|
||||||
df = frappe._dict(df)
|
df = frappe._dict(df)
|
||||||
if df.fieldname in frappe.db.get_table_columns(doctype) and \
|
if df.fieldname in frappe.db.get_table_columns(doctype) and \
|
||||||
frappe.db.sql("""select count(*) from `tab{doctype}`
|
frappe.db.count(dt=doctype, filters=IfNull(df.fieldname, "") != ""):
|
||||||
where ifnull({fieldname},'')!=''""".format(doctype=doctype, fieldname=df.fieldname))[0][0]:
|
|
||||||
|
|
||||||
create_custom_field(doctype, df)
|
create_custom_field(doctype, df)
|
||||||
|
|
||||||
def create_custom_field(doctype, df, ignore_validate=False):
|
def create_custom_field(doctype, df, ignore_validate=False):
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ frappe.ui.form.on("Customize Form", {
|
||||||
frm.page.clear_icons();
|
frm.page.clear_icons();
|
||||||
|
|
||||||
if (frm.doc.doc_type) {
|
if (frm.doc.doc_type) {
|
||||||
|
frm.page.set_title(__('Customize Form - {0}', [frm.doc.doc_type]));
|
||||||
frappe.customize_form.set_primary_action(frm);
|
frappe.customize_form.set_primary_action(frm);
|
||||||
|
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
|
|
@ -276,6 +277,21 @@ frappe.ui.form.on("DocType Action", {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// can't delete standard states
|
||||||
|
frappe.ui.form.on("DocType State", {
|
||||||
|
before_states_remove: function(frm, doctype, name) {
|
||||||
|
let row = frappe.get_doc(doctype, name);
|
||||||
|
if (!(row.custom || row.__islocal)) {
|
||||||
|
frappe.msgprint(__("Cannot delete standard document state."));
|
||||||
|
throw "cannot delete standard document state";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
states_add: function(frm, cdt, cdn) {
|
||||||
|
let f = frappe.model.get_doc(cdt, cdn);
|
||||||
|
f.custom = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
frappe.customize_form.set_primary_action = function(frm) {
|
frappe.customize_form.set_primary_action = function(frm) {
|
||||||
frm.page.set_primary_action(__("Update"), function() {
|
frm.page.set_primary_action(__("Update"), function() {
|
||||||
if (frm.doc.doc_type) {
|
if (frm.doc.doc_type) {
|
||||||
|
|
@ -332,3 +348,4 @@ frappe.customize_form.clear_locals_and_refresh = function(frm) {
|
||||||
frm.refresh();
|
frm.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({frm: cur_frm}));
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@
|
||||||
"actions",
|
"actions",
|
||||||
"document_links_section",
|
"document_links_section",
|
||||||
"links",
|
"links",
|
||||||
|
"document_states_section",
|
||||||
|
"states",
|
||||||
"section_break_8",
|
"section_break_8",
|
||||||
"sort_field",
|
"sort_field",
|
||||||
"column_break_10",
|
"column_break_10",
|
||||||
|
|
@ -280,6 +282,20 @@
|
||||||
"fieldname": "autoname",
|
"fieldname": "autoname",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Auto Name"
|
"label": "Auto Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"collapsible_depends_on": "states",
|
||||||
|
"depends_on": "doc_type",
|
||||||
|
"fieldname": "document_states_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Document States"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "states",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "States",
|
||||||
|
"options": "DocType State"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_toolbar": 1,
|
"hide_toolbar": 1,
|
||||||
|
|
@ -288,10 +304,11 @@
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-21 19:01:06.920663",
|
"modified": "2021-12-14 16:45:04.308690",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Customize Form",
|
"name": "Customize Form",
|
||||||
|
"naming_rule": "Expression (old style)",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
|
@ -308,5 +325,6 @@
|
||||||
"search_fields": "doc_type",
|
"search_fields": "doc_type",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +72,7 @@ class CustomizeForm(Document):
|
||||||
new_d[prop] = d.get(prop)
|
new_d[prop] = d.get(prop)
|
||||||
self.append("fields", new_d)
|
self.append("fields", new_d)
|
||||||
|
|
||||||
for fieldname in ('links', 'actions'):
|
for fieldname in ('links', 'actions', 'states'):
|
||||||
for d in meta.get(fieldname):
|
for d in meta.get(fieldname):
|
||||||
self.append(fieldname, d)
|
self.append(fieldname, d)
|
||||||
|
|
||||||
|
|
@ -258,7 +258,8 @@ class CustomizeForm(Document):
|
||||||
'''
|
'''
|
||||||
for doctype, fieldname, field_map in (
|
for doctype, fieldname, field_map in (
|
||||||
('DocType Link', 'links', doctype_link_properties),
|
('DocType Link', 'links', doctype_link_properties),
|
||||||
('DocType Action', 'actions', doctype_action_properties)
|
('DocType Action', 'actions', doctype_action_properties),
|
||||||
|
('DocType State', 'states', doctype_state_properties),
|
||||||
):
|
):
|
||||||
has_custom = False
|
has_custom = False
|
||||||
items = []
|
items = []
|
||||||
|
|
@ -568,6 +569,11 @@ doctype_action_properties = {
|
||||||
'hidden': 'Check'
|
'hidden': 'Check'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
doctype_state_properties = {
|
||||||
|
'title': 'Data',
|
||||||
|
'color': 'Select'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ALLOWED_FIELDTYPE_CHANGE = (
|
ALLOWED_FIELDTYPE_CHANGE = (
|
||||||
('Currency', 'Float', 'Percent'),
|
('Currency', 'Float', 'Percent'),
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Applied On",
|
"label": "Applied On",
|
||||||
"options": "\nDocField\nDocType\nDocType Link\nDocType Action",
|
"options": "\nDocField\nDocType\nDocType Link\nDocType Action\nDocType State",
|
||||||
"read_only_depends_on": "eval:!doc.__islocal",
|
"read_only_depends_on": "eval:!doc.__islocal",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
|
|
@ -109,7 +109,7 @@
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-04 12:46:17.860769",
|
"modified": "2021-12-14 14:15:41.929071",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Property Setter",
|
"name": "Property Setter",
|
||||||
|
|
@ -141,5 +141,6 @@
|
||||||
"search_fields": "doc_type,property",
|
"search_fields": "doc_type,property",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
79
frappe/custom/form_tour/custom_field/custom_field.json
Normal file
79
frappe/custom/form_tour/custom_field/custom_field.json
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
{
|
||||||
|
"creation": "2021-11-23 12:22:32.922700",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Form Tour",
|
||||||
|
"first_document": 0,
|
||||||
|
"idx": 0,
|
||||||
|
"include_name_field": 0,
|
||||||
|
"is_standard": 1,
|
||||||
|
"modified": "2021-11-24 19:15:34.244244",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Custom",
|
||||||
|
"name": "Custom Field",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"reference_doctype": "Custom Field",
|
||||||
|
"save_on_complete": 1,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"description": "Select a Document for which you want the Custom Field",
|
||||||
|
"field": "",
|
||||||
|
"fieldname": "dt",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"has_next_condition": 0,
|
||||||
|
"is_table_field": 0,
|
||||||
|
"label": "Document",
|
||||||
|
"parent_field": "",
|
||||||
|
"position": "Right",
|
||||||
|
"title": "Document"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enter a Label for this field",
|
||||||
|
"field": "",
|
||||||
|
"fieldname": "label",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"has_next_condition": 0,
|
||||||
|
"is_table_field": 0,
|
||||||
|
"label": "Label",
|
||||||
|
"parent_field": "",
|
||||||
|
"position": "Right",
|
||||||
|
"title": "Label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Select the label after which you want to insert new field.",
|
||||||
|
"field": "",
|
||||||
|
"fieldname": "insert_after",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"has_next_condition": 0,
|
||||||
|
"is_table_field": 0,
|
||||||
|
"label": "Insert After",
|
||||||
|
"parent_field": "",
|
||||||
|
"position": "Right",
|
||||||
|
"title": "Insert After"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Select an appropriate Field Type that suits your requirements",
|
||||||
|
"field": "",
|
||||||
|
"fieldname": "fieldtype",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"has_next_condition": 0,
|
||||||
|
"is_table_field": 0,
|
||||||
|
"label": "Field Type",
|
||||||
|
"parent_field": "",
|
||||||
|
"position": "Left",
|
||||||
|
"title": "Field Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Check this to make it a mandatory field",
|
||||||
|
"field": "",
|
||||||
|
"fieldname": "reqd",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"has_next_condition": 0,
|
||||||
|
"is_table_field": 0,
|
||||||
|
"label": "Is Mandatory Field",
|
||||||
|
"parent_field": "",
|
||||||
|
"position": "Left",
|
||||||
|
"title": "Is Mandatory Field"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Custom Field"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"allow_roles": [
|
||||||
|
{
|
||||||
|
"role": "All"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"creation": "2021-11-23 12:21:11.384229",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Module Onboarding",
|
||||||
|
"documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/customize-erpnext",
|
||||||
|
"idx": 0,
|
||||||
|
"is_complete": 0,
|
||||||
|
"modified": "2021-11-24 17:04:31.523715",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Custom",
|
||||||
|
"name": "Customization",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step": "Custom Field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Custom Doctype"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Naming Series"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Workflows"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Role Permissions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Print Format"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Report Builder"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subtitle": "Custom Field, Custom Doctype, Naming Series, Role Permission, Workflow, Print Formats, Reports",
|
||||||
|
"success_message": "Customization onboarding is all done!",
|
||||||
|
"title": "Customization"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"action": "Create Entry",
|
||||||
|
"action_label": "Learn more about creating new DocTypes",
|
||||||
|
"creation": "2021-11-23 12:30:04.407568",
|
||||||
|
"description": "A DocType (Document Type) is used to insert forms in ERPNext. Forms such as Customer, Orders, and Invoices are Doctypes in the backend. You can also create new DocTypes to create new forms in ERPNext as per your business needs.",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Onboarding Step",
|
||||||
|
"idx": 0,
|
||||||
|
"is_complete": 0,
|
||||||
|
"is_single": 0,
|
||||||
|
"is_skipped": 0,
|
||||||
|
"modified": "2021-11-23 12:30:04.407568",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"name": "Custom Doctype",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"reference_document": "DocType",
|
||||||
|
"show_form_tour": 1,
|
||||||
|
"show_full_form": 1,
|
||||||
|
"title": "Custom Document Types",
|
||||||
|
"validate_action": 1
|
||||||
|
}
|
||||||
21
frappe/custom/onboarding_step/custom_field/custom_field.json
Normal file
21
frappe/custom/onboarding_step/custom_field/custom_field.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"action": "Create Entry",
|
||||||
|
"action_label": "Learn how to add Custom Fields",
|
||||||
|
"creation": "2021-11-23 12:21:09.479808",
|
||||||
|
"description": "Every form in ERPNext has a standard set of fields. If you need to capture some information, but there is no standard Field available for it, you can insert Custom Field for it.\n\nOnce custom fields are added, you can use them for reports and analytics charts as well.\n",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Onboarding Step",
|
||||||
|
"idx": 0,
|
||||||
|
"is_complete": 0,
|
||||||
|
"is_single": 0,
|
||||||
|
"is_skipped": 0,
|
||||||
|
"modified": "2021-11-23 12:21:09.479808",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"name": "Custom Field",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"reference_document": "Custom Field",
|
||||||
|
"show_form_tour": 1,
|
||||||
|
"show_full_form": 1,
|
||||||
|
"title": "Create Custom Fields",
|
||||||
|
"validate_action": 1
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"action": "Watch Video",
|
||||||
|
"creation": "2021-11-23 13:57:45.091427",
|
||||||
|
"description": "Each document created in ERPNext can have a unique ID generated for it, using a prefix defined for it. Though each document has some prefix pre-configured, you can further customize it using tools like Naming Series Tool and Document Naming Rule.\n",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Onboarding Step",
|
||||||
|
"idx": 0,
|
||||||
|
"is_complete": 0,
|
||||||
|
"is_single": 0,
|
||||||
|
"is_skipped": 0,
|
||||||
|
"modified": "2021-11-24 15:04:14.662684",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"name": "Naming Series",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"show_form_tour": 0,
|
||||||
|
"show_full_form": 0,
|
||||||
|
"title": "Setup Naming Series",
|
||||||
|
"validate_action": 1,
|
||||||
|
"video_url": "https://youtu.be/IGyISSfI1qU"
|
||||||
|
}
|
||||||
21
frappe/custom/onboarding_step/print_format/print_format.json
Normal file
21
frappe/custom/onboarding_step/print_format/print_format.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"action": "Create Entry",
|
||||||
|
"action_label": "Learn about Standard and Custom Print Formats",
|
||||||
|
"creation": "2021-11-23 15:04:12.728513",
|
||||||
|
"description": "Print Formats allow you can define looks for documents when printed or converted to PDF. You can also create a custom Print Format using drag-and-drop tools.",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Onboarding Step",
|
||||||
|
"idx": 0,
|
||||||
|
"is_complete": 0,
|
||||||
|
"is_single": 0,
|
||||||
|
"is_skipped": 0,
|
||||||
|
"modified": "2021-11-23 15:04:12.728513",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"name": "Print Format",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"reference_document": "Print Format",
|
||||||
|
"show_form_tour": 1,
|
||||||
|
"show_full_form": 1,
|
||||||
|
"title": "Customize Print Formats",
|
||||||
|
"validate_action": 1
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"action": "Watch Video",
|
||||||
|
"action_label": "Learn more about Report Builders",
|
||||||
|
"creation": "2021-11-24 17:04:18.762838",
|
||||||
|
"description": "In each module, you will find a host of single-click reports, ranging from financial statements to sales and purchase analytics and stock tracking reports. If a required new report is not available out-of-the-box, you can create custom reports in ERPNext by pulling values from the same multiple ERPNext tables.\n",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Onboarding Step",
|
||||||
|
"idx": 0,
|
||||||
|
"is_complete": 0,
|
||||||
|
"is_single": 0,
|
||||||
|
"is_skipped": 0,
|
||||||
|
"modified": "2021-11-24 17:04:18.762838",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"name": "Report Builder",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"reference_document": "Report",
|
||||||
|
"show_form_tour": 0,
|
||||||
|
"show_full_form": 0,
|
||||||
|
"title": "Generate Custom Reports",
|
||||||
|
"validate_action": 1,
|
||||||
|
"video_url": "https://youtu.be/TxJGUNarcQs"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"action": "Watch Video",
|
||||||
|
"creation": "2021-11-23 14:00:27.208500",
|
||||||
|
"description": "In ERPNext, you can add your Employees as Users, and give them restricted access. Tools like Role Permission and User Permission allow you to define rules which give restricted access to the user to masters and transactions.",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Onboarding Step",
|
||||||
|
"idx": 0,
|
||||||
|
"is_complete": 0,
|
||||||
|
"is_single": 0,
|
||||||
|
"is_skipped": 0,
|
||||||
|
"modified": "2021-11-24 15:04:14.615232",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"name": "Role Permissions",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"show_form_tour": 0,
|
||||||
|
"show_full_form": 0,
|
||||||
|
"title": "Setup Limited Access for a User",
|
||||||
|
"validate_action": 1,
|
||||||
|
"video_url": "https://youtu.be/g3mk45o1zAg"
|
||||||
|
}
|
||||||
20
frappe/custom/onboarding_step/workflows/workflows.json
Normal file
20
frappe/custom/onboarding_step/workflows/workflows.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"action": "Watch Video",
|
||||||
|
"creation": "2021-11-23 13:58:58.530044",
|
||||||
|
"description": "Workflows allow you to define custom rules for the approval process of a particular document in ERPNext. You can also set complex Workflow Rules and set approval conditions.",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Onboarding Step",
|
||||||
|
"idx": 0,
|
||||||
|
"is_complete": 0,
|
||||||
|
"is_single": 0,
|
||||||
|
"is_skipped": 0,
|
||||||
|
"modified": "2021-11-24 15:04:14.632144",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"name": "Workflows",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"show_form_tour": 0,
|
||||||
|
"show_full_form": 0,
|
||||||
|
"title": "Setup Approval Workflows",
|
||||||
|
"validate_action": 1,
|
||||||
|
"video_url": "https://youtu.be/yObJUg9FxFs"
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"charts": [],
|
"charts": [],
|
||||||
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Customize Form\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Custom Role\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Client Script\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Server Script\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Dashboards\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Form Customization\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other\", \"col\": 4}}]",
|
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Customization\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customize Form\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Custom Role\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Client Script\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Server Script\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Dashboards\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Form Customization\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other\",\"col\":4}}]",
|
||||||
"creation": "2020-03-02 15:15:03.839594",
|
"creation": "2020-03-02 15:15:03.839594",
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Workspace",
|
"doctype": "Workspace",
|
||||||
|
|
@ -123,7 +123,7 @@
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-08-05 12:15:57.486113",
|
"modified": "2021-11-24 16:20:03.500885",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Custom",
|
"module": "Custom",
|
||||||
"name": "Customization",
|
"name": "Customization",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||||
# Copyright (c) 2017, Frappe Technologies and contributors
|
|
||||||
# License: MIT. See LICENSE
|
# License: MIT. See LICENSE
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
@ -8,6 +7,20 @@ from frappe.modules.export_file import export_to_files, create_init_py
|
||||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
def get_mapping_module(module, mapping_name):
|
||||||
|
app_name = frappe.db.get_value("Module Def", module, "app_name")
|
||||||
|
mapping_name = frappe.scrub(mapping_name)
|
||||||
|
module = frappe.scrub(module)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return frappe.get_module(
|
||||||
|
f"{app_name}.{module}.data_migration_mapping.{mapping_name}"
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class DataMigrationPlan(Document):
|
class DataMigrationPlan(Document):
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
# update custom fields in mappings
|
# update custom fields in mappings
|
||||||
|
|
@ -54,26 +67,14 @@ class DataMigrationPlan(Document):
|
||||||
frappe.flags.ignore_in_install = False
|
frappe.flags.ignore_in_install = False
|
||||||
|
|
||||||
def pre_process_doc(self, mapping_name, doc):
|
def pre_process_doc(self, mapping_name, doc):
|
||||||
module = self.get_mapping_module(mapping_name)
|
module = get_mapping_module(self.module, mapping_name)
|
||||||
|
|
||||||
if module and hasattr(module, 'pre_process'):
|
if module and hasattr(module, 'pre_process'):
|
||||||
return module.pre_process(doc)
|
return module.pre_process(doc)
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
def post_process_doc(self, mapping_name, local_doc=None, remote_doc=None):
|
def post_process_doc(self, mapping_name, local_doc=None, remote_doc=None):
|
||||||
module = self.get_mapping_module(mapping_name)
|
module = get_mapping_module(self.module, mapping_name)
|
||||||
|
|
||||||
if module and hasattr(module, 'post_process'):
|
if module and hasattr(module, 'post_process'):
|
||||||
return module.post_process(local_doc=local_doc, remote_doc=remote_doc)
|
return module.post_process(local_doc=local_doc, remote_doc=remote_doc)
|
||||||
|
|
||||||
def get_mapping_module(self, mapping_name):
|
|
||||||
try:
|
|
||||||
module_def = frappe.get_doc("Module Def", self.module)
|
|
||||||
module = frappe.get_module('{app}.{module}.data_migration_mapping.{mapping_name}'.format(
|
|
||||||
app= module_def.app_name,
|
|
||||||
module=frappe.scrub(self.module),
|
|
||||||
mapping_name=frappe.scrub(mapping_name)
|
|
||||||
))
|
|
||||||
return module
|
|
||||||
except ImportError:
|
|
||||||
return None
|
|
||||||
|
|
|
||||||
|
|
@ -171,10 +171,10 @@ class Database(object):
|
||||||
frappe.errprint(query)
|
frappe.errprint(query)
|
||||||
|
|
||||||
elif self.is_deadlocked(e):
|
elif self.is_deadlocked(e):
|
||||||
raise frappe.QueryDeadlockError
|
raise frappe.QueryDeadlockError(e)
|
||||||
|
|
||||||
elif self.is_timedout(e):
|
elif self.is_timedout(e):
|
||||||
raise frappe.QueryTimeoutError
|
raise frappe.QueryTimeoutError(e)
|
||||||
|
|
||||||
if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)):
|
if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)):
|
||||||
pass
|
pass
|
||||||
|
|
@ -260,6 +260,7 @@ class Database(object):
|
||||||
self.commit()
|
self.commit()
|
||||||
self.sql(query, debug=debug)
|
self.sql(query, debug=debug)
|
||||||
|
|
||||||
|
|
||||||
def check_transaction_status(self, query):
|
def check_transaction_status(self, query):
|
||||||
"""Raises exception if more than 20,000 `INSERT`, `UPDATE` queries are
|
"""Raises exception if more than 20,000 `INSERT`, `UPDATE` queries are
|
||||||
executed in one transaction. This is to ensure that writes are always flushed otherwise this
|
executed in one transaction. This is to ensure that writes are always flushed otherwise this
|
||||||
|
|
@ -334,8 +335,21 @@ class Database(object):
|
||||||
"""Returns `get_value` with fieldname='*'"""
|
"""Returns `get_value` with fieldname='*'"""
|
||||||
return self.get_value(doctype, filters, "*", as_dict=as_dict, cache=cache)
|
return self.get_value(doctype, filters, "*", as_dict=as_dict, cache=cache)
|
||||||
|
|
||||||
def get_value(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
|
def get_value(
|
||||||
debug=False, order_by=None, cache=False, for_update=False, run=True):
|
self,
|
||||||
|
doctype,
|
||||||
|
filters=None,
|
||||||
|
fieldname="name",
|
||||||
|
ignore=None,
|
||||||
|
as_dict=False,
|
||||||
|
debug=False,
|
||||||
|
order_by="KEEP_DEFAULT_ORDERING",
|
||||||
|
cache=False,
|
||||||
|
for_update=False,
|
||||||
|
run=True,
|
||||||
|
pluck=False,
|
||||||
|
distinct=False,
|
||||||
|
):
|
||||||
"""Returns a document property or list of properties.
|
"""Returns a document property or list of properties.
|
||||||
|
|
||||||
:param doctype: DocType name.
|
:param doctype: DocType name.
|
||||||
|
|
@ -362,7 +376,7 @@ class Database(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug,
|
ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug,
|
||||||
order_by, cache=cache, for_update=for_update, run=run)
|
order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct)
|
||||||
|
|
||||||
if not run:
|
if not run:
|
||||||
return ret
|
return ret
|
||||||
|
|
@ -370,7 +384,8 @@ class Database(object):
|
||||||
return ((len(ret[0]) > 1 or as_dict) and ret[0] or ret[0][0]) if ret else None
|
return ((len(ret[0]) > 1 or as_dict) and ret[0] or ret[0][0]) if ret else None
|
||||||
|
|
||||||
def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
|
def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
|
||||||
debug=False, order_by=None, update=None, cache=False, for_update=False, run=True):
|
debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False,
|
||||||
|
run=True, pluck=False, distinct=False):
|
||||||
"""Returns multiple document properties.
|
"""Returns multiple document properties.
|
||||||
|
|
||||||
:param doctype: DocType name.
|
:param doctype: DocType name.
|
||||||
|
|
@ -379,7 +394,8 @@ class Database(object):
|
||||||
:param ignore: Don't raise exception if table, column is missing.
|
:param ignore: Don't raise exception if table, column is missing.
|
||||||
:param as_dict: Return values as dict.
|
:param as_dict: Return values as dict.
|
||||||
:param debug: Print query in error log.
|
:param debug: Print query in error log.
|
||||||
:param order_by: Column to order by
|
:param order_by: Column to order by,
|
||||||
|
:param distinct: Get Distinct results.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
|
@ -394,9 +410,20 @@ class Database(object):
|
||||||
(doctype, filters, fieldname) in self.value_cache:
|
(doctype, filters, fieldname) in self.value_cache:
|
||||||
return self.value_cache[(doctype, filters, fieldname)]
|
return self.value_cache[(doctype, filters, fieldname)]
|
||||||
|
|
||||||
|
if distinct:
|
||||||
|
order_by = None
|
||||||
|
|
||||||
if isinstance(filters, list):
|
if isinstance(filters, list):
|
||||||
order_by = order_by or "modified_desc"
|
out = self._get_value_for_many_names(
|
||||||
out = self._get_value_for_many_names(doctype, filters, fieldname, debug=debug, run=run)
|
doctype,
|
||||||
|
filters,
|
||||||
|
fieldname,
|
||||||
|
order_by,
|
||||||
|
debug=debug,
|
||||||
|
run=run,
|
||||||
|
pluck=pluck,
|
||||||
|
distinct=distinct,
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
fields = fieldname
|
fields = fieldname
|
||||||
|
|
@ -408,9 +435,20 @@ class Database(object):
|
||||||
|
|
||||||
if (filters is not None) and (filters!=doctype or doctype=="DocType"):
|
if (filters is not None) and (filters!=doctype or doctype=="DocType"):
|
||||||
try:
|
try:
|
||||||
order_by = order_by or "modified"
|
if order_by:
|
||||||
|
order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by
|
||||||
out = self._get_values_from_table(
|
out = self._get_values_from_table(
|
||||||
fields, filters, doctype, as_dict, debug, order_by, update, for_update=for_update, run=run
|
fields,
|
||||||
|
filters,
|
||||||
|
doctype,
|
||||||
|
as_dict,
|
||||||
|
debug,
|
||||||
|
order_by,
|
||||||
|
update,
|
||||||
|
for_update=for_update,
|
||||||
|
run=run,
|
||||||
|
pluck=pluck,
|
||||||
|
distinct=distinct
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)):
|
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)):
|
||||||
|
|
@ -418,19 +456,30 @@ class Database(object):
|
||||||
out = None
|
out = None
|
||||||
elif (not ignore) and frappe.db.is_table_missing(e):
|
elif (not ignore) and frappe.db.is_table_missing(e):
|
||||||
# table not found, look in singles
|
# table not found, look in singles
|
||||||
out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update, run=run)
|
out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update, run=run, distinct=distinct)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update, run=run)
|
out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update, run=run, pluck=pluck, distinct=distinct)
|
||||||
|
|
||||||
if cache and isinstance(filters, str):
|
if cache and isinstance(filters, str):
|
||||||
self.value_cache[(doctype, filters, fieldname)] = out
|
self.value_cache[(doctype, filters, fieldname)] = out
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def get_values_from_single(self, fields, filters, doctype, as_dict=False, debug=False, update=None, run=True):
|
def get_values_from_single(
|
||||||
|
self,
|
||||||
|
fields,
|
||||||
|
filters,
|
||||||
|
doctype,
|
||||||
|
as_dict=False,
|
||||||
|
debug=False,
|
||||||
|
update=None,
|
||||||
|
run=True,
|
||||||
|
pluck=False,
|
||||||
|
distinct=False,
|
||||||
|
):
|
||||||
"""Get values from `tabSingles` (Single DocTypes) (internal).
|
"""Get values from `tabSingles` (Single DocTypes) (internal).
|
||||||
|
|
||||||
:param fields: List of fields,
|
:param fields: List of fields,
|
||||||
|
|
@ -456,10 +505,13 @@ class Database(object):
|
||||||
return [map(values.get, fields)]
|
return [map(values.get, fields)]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
r = self.sql("""select field, value
|
r = self.query.get_sql(
|
||||||
from `tabSingles` where field in (%s) and doctype=%s"""
|
"Singles",
|
||||||
% (', '.join(['%s'] * len(fields)), '%s'),
|
filters={"field": ("in", tuple(fields)), "doctype": doctype},
|
||||||
tuple(fields) + (doctype,), as_dict=False, debug=debug, run=run)
|
fields=["field", "value"],
|
||||||
|
distinct=distinct,
|
||||||
|
).run(pluck=pluck, debug=debug, as_dict=False)
|
||||||
|
|
||||||
if not run:
|
if not run:
|
||||||
return r
|
return r
|
||||||
if as_dict:
|
if as_dict:
|
||||||
|
|
@ -484,14 +536,10 @@ class Database(object):
|
||||||
# Get coulmn and value of the single doctype Accounts Settings
|
# Get coulmn and value of the single doctype Accounts Settings
|
||||||
account_settings = frappe.db.get_singles_dict("Accounts Settings")
|
account_settings = frappe.db.get_singles_dict("Accounts Settings")
|
||||||
"""
|
"""
|
||||||
result = self.sql("""
|
result = self.query.get_sql(
|
||||||
SELECT field, value
|
"Singles", filters={"doctype": doctype}, fields=["field", "value"]
|
||||||
FROM `tabSingles`
|
).run()
|
||||||
WHERE doctype = %s
|
|
||||||
""", doctype)
|
|
||||||
|
|
||||||
dict_ = frappe._dict(result)
|
dict_ = frappe._dict(result)
|
||||||
|
|
||||||
return dict_
|
return dict_
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -520,8 +568,11 @@ class Database(object):
|
||||||
if fieldname in self.value_cache[doctype]:
|
if fieldname in self.value_cache[doctype]:
|
||||||
return self.value_cache[doctype][fieldname]
|
return self.value_cache[doctype][fieldname]
|
||||||
|
|
||||||
val = self.sql("""select `value` from
|
val = self.query.get_sql(
|
||||||
`tabSingles` where `doctype`=%s and `field`=%s""", (doctype, fieldname))
|
table="Singles",
|
||||||
|
filters={"doctype": doctype, "field": fieldname},
|
||||||
|
fields="value",
|
||||||
|
).run()
|
||||||
val = val[0][0] if val else None
|
val = val[0][0] if val else None
|
||||||
|
|
||||||
df = frappe.get_meta(doctype).get_field(fieldname)
|
df = frappe.get_meta(doctype).get_field(fieldname)
|
||||||
|
|
@ -539,40 +590,64 @@ class Database(object):
|
||||||
"""Alias for get_single_value"""
|
"""Alias for get_single_value"""
|
||||||
return self.get_single_value(*args, **kwargs)
|
return self.get_single_value(*args, **kwargs)
|
||||||
|
|
||||||
def _get_values_from_table(self, fields, filters, doctype, as_dict, debug, order_by=None,
|
def _get_values_from_table(
|
||||||
update=None, for_update=False, run=True):
|
self,
|
||||||
|
fields,
|
||||||
|
filters,
|
||||||
|
doctype,
|
||||||
|
as_dict,
|
||||||
|
debug,
|
||||||
|
order_by=None,
|
||||||
|
update=None,
|
||||||
|
for_update=False,
|
||||||
|
run=True,
|
||||||
|
pluck=False,
|
||||||
|
distinct=False,
|
||||||
|
):
|
||||||
field_objects = []
|
field_objects = []
|
||||||
|
|
||||||
if not isinstance(fields, Criterion):
|
if not isinstance(fields, Criterion):
|
||||||
for field in fields:
|
for field in fields:
|
||||||
if "(" in field or " as " in field:
|
if "(" in str(field) or " as " in str(field):
|
||||||
field_objects.append(PseudoColumn(field))
|
field_objects.append(PseudoColumn(field))
|
||||||
else:
|
else:
|
||||||
field_objects.append(field)
|
field_objects.append(field)
|
||||||
|
|
||||||
criterion = self.query.build_conditions(
|
query = self.query.get_sql(
|
||||||
table=doctype, filters=filters, orderby=order_by, for_update=for_update
|
table=doctype,
|
||||||
|
filters=filters,
|
||||||
|
orderby=order_by,
|
||||||
|
for_update=for_update,
|
||||||
|
field_objects=field_objects,
|
||||||
|
fields=fields,
|
||||||
|
distinct=distinct,
|
||||||
)
|
)
|
||||||
if isinstance(fields, (list, tuple)):
|
if (
|
||||||
query = criterion.select(*field_objects)
|
fields == "*"
|
||||||
|
and not isinstance(fields, (list, tuple))
|
||||||
|
and not isinstance(fields, Criterion)
|
||||||
|
):
|
||||||
|
as_dict = True
|
||||||
|
|
||||||
elif isinstance(fields, Criterion):
|
r = self.sql(
|
||||||
query = criterion.select(fields)
|
query, as_dict=as_dict, debug=debug, update=update, run=run, pluck=pluck
|
||||||
|
)
|
||||||
else:
|
|
||||||
if fields=="*":
|
|
||||||
query = criterion.select(fields)
|
|
||||||
as_dict = True
|
|
||||||
r = self.sql(query, as_dict=as_dict, debug=debug, update=update, run=run)
|
|
||||||
return r
|
return r
|
||||||
|
|
||||||
def _get_value_for_many_names(self, doctype, names, field, debug=False, run=True):
|
def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False):
|
||||||
names = list(filter(None, names))
|
names = list(filter(None, names))
|
||||||
if names:
|
if names:
|
||||||
return self.get_all(doctype,
|
return self.get_all(
|
||||||
|
doctype,
|
||||||
fields=field,
|
fields=field,
|
||||||
filters=names,
|
filters=names,
|
||||||
debug=debug, as_list=1, run=run)
|
order_by=order_by,
|
||||||
|
pluck=pluck,
|
||||||
|
debug=debug,
|
||||||
|
as_list=1,
|
||||||
|
run=run,
|
||||||
|
distinct=distinct,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
@ -788,25 +863,13 @@ class Database(object):
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def min(self, dt, fieldname, filters=None, **kwargs):
|
|
||||||
return self.query.build_conditions(dt, filters=filters).select(Min(Column(fieldname))).run(**kwargs)[0][0] or 0
|
|
||||||
|
|
||||||
def max(self, dt, fieldname, filters=None, **kwargs):
|
|
||||||
return self.query.build_conditions(dt, filters=filters).select(Max(Column(fieldname))).run(**kwargs)[0][0] or 0
|
|
||||||
|
|
||||||
def avg(self, dt, fieldname, filters=None, **kwargs):
|
|
||||||
return self.query.build_conditions(dt, filters=filters).select(Avg(Column(fieldname))).run(**kwargs)[0][0] or 0
|
|
||||||
|
|
||||||
def sum(self, dt, fieldname, filters=None, **kwargs):
|
|
||||||
return self.query.build_conditions(dt, filters=filters).select(Sum(Column(fieldname))).run(**kwargs)[0][0] or 0
|
|
||||||
|
|
||||||
def count(self, dt, filters=None, debug=False, cache=False):
|
def count(self, dt, filters=None, debug=False, cache=False):
|
||||||
"""Returns `COUNT(*)` for given DocType and filters."""
|
"""Returns `COUNT(*)` for given DocType and filters."""
|
||||||
if cache and not filters:
|
if cache and not filters:
|
||||||
cache_count = frappe.cache().get_value('doctype:count:{}'.format(dt))
|
cache_count = frappe.cache().get_value('doctype:count:{}'.format(dt))
|
||||||
if cache_count is not None:
|
if cache_count is not None:
|
||||||
return cache_count
|
return cache_count
|
||||||
query = self.query.build_conditions(table=dt, filters=filters).select(Count("*"))
|
query = self.query.get_sql(table=dt, filters=filters, fields=Count("*"))
|
||||||
if filters:
|
if filters:
|
||||||
count = self.sql(query, debug=debug)[0][0]
|
count = self.sql(query, debug=debug)[0][0]
|
||||||
return count
|
return count
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ class MariaDBDatabase(Database):
|
||||||
'Dynamic Link': ('varchar', self.VARCHAR_LEN),
|
'Dynamic Link': ('varchar', self.VARCHAR_LEN),
|
||||||
'Password': ('text', ''),
|
'Password': ('text', ''),
|
||||||
'Select': ('varchar', self.VARCHAR_LEN),
|
'Select': ('varchar', self.VARCHAR_LEN),
|
||||||
'Rating': ('int', '1'),
|
'Rating': ('decimal', '3,2'),
|
||||||
'Read Only': ('varchar', self.VARCHAR_LEN),
|
'Read Only': ('varchar', self.VARCHAR_LEN),
|
||||||
'Attach': ('text', ''),
|
'Attach': ('text', ''),
|
||||||
'Attach Image': ('text', ''),
|
'Attach Image': ('text', ''),
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ class PostgresDatabase(Database):
|
||||||
'Dynamic Link': ('varchar', self.VARCHAR_LEN),
|
'Dynamic Link': ('varchar', self.VARCHAR_LEN),
|
||||||
'Password': ('text', ''),
|
'Password': ('text', ''),
|
||||||
'Select': ('varchar', self.VARCHAR_LEN),
|
'Select': ('varchar', self.VARCHAR_LEN),
|
||||||
'Rating': ('smallint', None),
|
'Rating': ('decimal', '3,2'),
|
||||||
'Read Only': ('varchar', self.VARCHAR_LEN),
|
'Read Only': ('varchar', self.VARCHAR_LEN),
|
||||||
'Attach': ('text', ''),
|
'Attach': ('text', ''),
|
||||||
'Attach Image': ('text', ''),
|
'Attach Image': ('text', ''),
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import operator
|
import operator
|
||||||
|
import re
|
||||||
from typing import Any, Dict, List, Tuple, Union
|
from typing import Any, Dict, List, Tuple, Union
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.query_builder import Criterion, Order, Field
|
from frappe import _
|
||||||
|
from frappe.query_builder import Criterion, Field, Order
|
||||||
|
|
||||||
|
|
||||||
def like(key: str, value: str) -> frappe.qb:
|
def like(key: str, value: str) -> frappe.qb:
|
||||||
|
|
@ -224,6 +226,7 @@ class Query:
|
||||||
"""
|
"""
|
||||||
conditions = self.get_condition(table, **kwargs)
|
conditions = self.get_condition(table, **kwargs)
|
||||||
if not filters:
|
if not filters:
|
||||||
|
conditions = self.add_conditions(conditions, **kwargs)
|
||||||
return conditions
|
return conditions
|
||||||
|
|
||||||
for key in filters:
|
for key in filters:
|
||||||
|
|
@ -245,7 +248,12 @@ class Query:
|
||||||
conditions = self.add_conditions(conditions, **kwargs)
|
conditions = self.add_conditions(conditions, **kwargs)
|
||||||
return conditions
|
return conditions
|
||||||
|
|
||||||
def build_conditions(self, table: str, filters: Union[Dict[str, Union[str, int]], str, int] = None, **kwargs) -> frappe.qb:
|
def build_conditions(
|
||||||
|
self,
|
||||||
|
table: str,
|
||||||
|
filters: Union[Dict[str, Union[str, int]], str, int] = None,
|
||||||
|
**kwargs
|
||||||
|
) -> frappe.qb:
|
||||||
"""Build conditions for sql query
|
"""Build conditions for sql query
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -255,13 +263,67 @@ class Query:
|
||||||
Returns:
|
Returns:
|
||||||
frappe.qb: frappe.qb conditions object
|
frappe.qb: frappe.qb conditions object
|
||||||
"""
|
"""
|
||||||
if isinstance(filters, Criterion):
|
|
||||||
return self.criterion_query(table, filters, **kwargs)
|
|
||||||
|
|
||||||
if isinstance(filters, int) or isinstance(filters, str):
|
if isinstance(filters, int) or isinstance(filters, str):
|
||||||
filters = {"name": str(filters)}
|
filters = {"name": str(filters)}
|
||||||
|
|
||||||
if isinstance(filters, (list, tuple)):
|
if isinstance(filters, Criterion):
|
||||||
return self.misc_query(table, filters, **kwargs)
|
criterion = self.criterion_query(table, filters, **kwargs)
|
||||||
|
|
||||||
return self.dict_query(filters=filters, table=table, **kwargs)
|
elif isinstance(filters, (list, tuple)):
|
||||||
|
criterion = self.misc_query(table, filters, **kwargs)
|
||||||
|
|
||||||
|
else:
|
||||||
|
criterion = self.dict_query(filters=filters, table=table, **kwargs)
|
||||||
|
|
||||||
|
return criterion
|
||||||
|
|
||||||
|
def get_sql(
|
||||||
|
self,
|
||||||
|
table: str,
|
||||||
|
fields: Union[List, Tuple],
|
||||||
|
filters: Union[Dict[str, Union[str, int]], str, int] = None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
criterion = self.build_conditions(table, filters, **kwargs)
|
||||||
|
if isinstance(fields, (list, tuple)):
|
||||||
|
query = criterion.select(*kwargs.get("field_objects", fields))
|
||||||
|
|
||||||
|
elif isinstance(fields, Criterion):
|
||||||
|
query = criterion.select(fields)
|
||||||
|
|
||||||
|
else:
|
||||||
|
query = criterion.select(fields)
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
class Permission:
|
||||||
|
@classmethod
|
||||||
|
def check_permissions(cls, query, **kwargs):
|
||||||
|
if not isinstance(query, str):
|
||||||
|
query = query.get_sql()
|
||||||
|
|
||||||
|
doctype = cls.get_tables_from_query(query)
|
||||||
|
if isinstance(doctype, str):
|
||||||
|
doctype = [doctype]
|
||||||
|
|
||||||
|
for dt in doctype:
|
||||||
|
dt = re.sub("tab", "", dt)
|
||||||
|
if not frappe.has_permission(
|
||||||
|
dt,
|
||||||
|
"select",
|
||||||
|
user=kwargs.get("user"),
|
||||||
|
parent_doctype=kwargs.get("parent_doctype"),
|
||||||
|
) and not frappe.has_permission(
|
||||||
|
dt,
|
||||||
|
"read",
|
||||||
|
user=kwargs.get("user"),
|
||||||
|
parent_doctype=kwargs.get("parent_doctype"),
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_("Insufficient Permission for {0}").format(frappe.bold(dt))
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_tables_from_query(query: str):
|
||||||
|
return [table for table in re.findall(r"\w+", query) if table.startswith("tab")]
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ from frappe.utils import cstr
|
||||||
|
|
||||||
queue_prefix = 'insert_queue_for_'
|
queue_prefix = 'insert_queue_for_'
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def deferred_insert(doctype, records):
|
def deferred_insert(doctype, records):
|
||||||
frappe.cache().rpush(queue_prefix + doctype, records)
|
frappe.cache().rpush(queue_prefix + doctype, records)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,13 @@ frappe.ui.form.on('Form Tour', {
|
||||||
|
|
||||||
frm.add_custom_button(__('Show Tour'), async () => {
|
frm.add_custom_button(__('Show Tour'), async () => {
|
||||||
const issingle = await check_if_single(frm.doc.reference_doctype);
|
const issingle = await check_if_single(frm.doc.reference_doctype);
|
||||||
|
const name = await get_first_document(frm.doc.reference_doctype);
|
||||||
let route_changed = null;
|
let route_changed = null;
|
||||||
|
|
||||||
if (issingle) {
|
if (issingle) {
|
||||||
route_changed = frappe.set_route('Form', frm.doc.reference_doctype);
|
route_changed = frappe.set_route('Form', frm.doc.reference_doctype);
|
||||||
|
} else if (frm.doc.first_document) {
|
||||||
|
route_changed = frappe.set_route('Form', frm.doc.reference_doctype, name);
|
||||||
} else {
|
} else {
|
||||||
route_changed = frappe.set_route('Form', frm.doc.reference_doctype, 'new');
|
route_changed = frappe.set_route('Form', frm.doc.reference_doctype, 'new');
|
||||||
}
|
}
|
||||||
|
|
@ -120,4 +123,15 @@ function get_child_field(child_table, child_name, fieldname) {
|
||||||
async function check_if_single(doctype) {
|
async function check_if_single(doctype) {
|
||||||
const { message } = await frappe.db.get_value('DocType', doctype, 'issingle');
|
const { message } = await frappe.db.get_value('DocType', doctype, 'issingle');
|
||||||
return message.issingle || 0;
|
return message.issingle || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function get_first_document(doctype) {
|
||||||
|
let docname;
|
||||||
|
|
||||||
|
await frappe.db.get_list(doctype, { order_by: "creation" }).then(res => {
|
||||||
|
if (Array.isArray(res) && res.length)
|
||||||
|
docname = res[0].name;
|
||||||
|
});
|
||||||
|
|
||||||
|
return docname || 'new';
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,11 @@
|
||||||
"title",
|
"title",
|
||||||
"reference_doctype",
|
"reference_doctype",
|
||||||
"module",
|
"module",
|
||||||
|
"column_break_6",
|
||||||
"is_standard",
|
"is_standard",
|
||||||
"save_on_complete",
|
"save_on_complete",
|
||||||
|
"first_document",
|
||||||
|
"include_name_field",
|
||||||
"section_break_3",
|
"section_break_3",
|
||||||
"steps"
|
"steps"
|
||||||
],
|
],
|
||||||
|
|
@ -62,14 +65,32 @@
|
||||||
"label": "Module",
|
"label": "Module",
|
||||||
"options": "Module Def",
|
"options": "Module Def",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_6",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "first_document",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Show First Document Tour"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval:!doc.first_document",
|
||||||
|
"fieldname": "include_name_field",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Include Name Field"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-06 20:32:54.068774",
|
"modified": "2021-11-24 12:03:45.449311",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Desk",
|
"module": "Desk",
|
||||||
"name": "Form Tour",
|
"name": "Form Tour",
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class GlobalSearchSettings(Document):
|
||||||
|
|
||||||
def get_doctypes_for_global_search():
|
def get_doctypes_for_global_search():
|
||||||
def get_from_db():
|
def get_from_db():
|
||||||
doctypes = frappe.get_list("Global Search DocType", fields=["document_type"], order_by="idx ASC")
|
doctypes = frappe.get_all("Global Search DocType", fields=["document_type"], order_by="idx ASC")
|
||||||
return [d.document_type for d in doctypes] or []
|
return [d.document_type for d in doctypes] or []
|
||||||
|
|
||||||
return frappe.cache().hget("global_search", "search_priorities", get_from_db)
|
return frappe.cache().hget("global_search", "search_priorities", get_from_db)
|
||||||
|
|
|
||||||
|
|
@ -1,155 +1,55 @@
|
||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_import": 0,
|
"creation": "2016-10-19 12:26:42.569185",
|
||||||
"allow_rename": 0,
|
"doctype": "DocType",
|
||||||
"beta": 0,
|
"editable_grid": 1,
|
||||||
"creation": "2016-10-19 12:26:42.569185",
|
"engine": "InnoDB",
|
||||||
"custom": 0,
|
"field_order": [
|
||||||
"docstatus": 0,
|
"column_name",
|
||||||
"doctype": "DocType",
|
"status",
|
||||||
"document_type": "",
|
"indicator",
|
||||||
"editable_grid": 1,
|
"order"
|
||||||
"engine": "InnoDB",
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "column_name",
|
||||||
"bold": 0,
|
"fieldtype": "Data",
|
||||||
"collapsible": 0,
|
"in_list_view": 1,
|
||||||
"columns": 0,
|
"label": "Column Name"
|
||||||
"fieldname": "column_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": "Column Name",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"default": "Active",
|
||||||
"bold": 0,
|
"fieldname": "status",
|
||||||
"collapsible": 0,
|
"fieldtype": "Select",
|
||||||
"columns": 0,
|
"in_list_view": 1,
|
||||||
"default": "Active",
|
"label": "Status",
|
||||||
"fieldname": "status",
|
"options": "Active\nArchived"
|
||||||
"fieldtype": "Select",
|
},
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Status",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Active\nArchived",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"default": "Gray",
|
||||||
"bold": 0,
|
"fieldname": "indicator",
|
||||||
"collapsible": 0,
|
"fieldtype": "Select",
|
||||||
"columns": 0,
|
"in_list_view": 1,
|
||||||
"default": "darkgrey",
|
"label": "Indicator",
|
||||||
"fieldname": "indicator",
|
"options": "Blue\nCyan\nGray\nGreen\nLight Blue\nOrange\nPink\nPurple\nRed\nRed\nYellow"
|
||||||
"fieldtype": "Select",
|
},
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Indicator",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "blue\norange\nred\ngreen\ndarkgrey\npurple\nyellow\nlightblue",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "order",
|
||||||
"bold": 0,
|
"fieldtype": "Code",
|
||||||
"collapsible": 0,
|
"label": "Order"
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "order",
|
|
||||||
"fieldtype": "Code",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Order",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_heading": 0,
|
"istable": 1,
|
||||||
"hide_toolbar": 0,
|
"links": [],
|
||||||
"idx": 0,
|
"modified": "2021-12-14 13:13:38.804259",
|
||||||
"image_view": 0,
|
"modified_by": "Administrator",
|
||||||
"in_create": 0,
|
"module": "Desk",
|
||||||
|
"name": "Kanban Board Column",
|
||||||
"is_submittable": 0,
|
"owner": "Administrator",
|
||||||
"issingle": 0,
|
"permissions": [],
|
||||||
"istable": 1,
|
"quick_entry": 1,
|
||||||
"max_attachments": 0,
|
"sort_field": "modified",
|
||||||
"modified": "2017-01-17 15:23:43.520379",
|
"sort_order": "DESC",
|
||||||
"modified_by": "Administrator",
|
"states": [],
|
||||||
"module": "Desk",
|
"track_changes": 1
|
||||||
"name": "Kanban Board Column",
|
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [],
|
|
||||||
"quick_entry": 1,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"track_changes": 1,
|
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
.like-disabled-input{
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
@ -2,6 +2,17 @@
|
||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on("Onboarding Step", {
|
frappe.ui.form.on("Onboarding Step", {
|
||||||
|
|
||||||
|
setup: function(frm) {
|
||||||
|
frm.set_query("form_tour", function() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
reference_doctype: frm.doc.reference_document
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
frappe.boot.developer_mode &&
|
frappe.boot.developer_mode &&
|
||||||
frm.set_intro(
|
frm.set_intro(
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"reference_document",
|
"reference_document",
|
||||||
"show_full_form",
|
"show_full_form",
|
||||||
"show_form_tour",
|
"show_form_tour",
|
||||||
|
"form_tour",
|
||||||
"is_single",
|
"is_single",
|
||||||
"reference_report",
|
"reference_report",
|
||||||
"report_reference_doctype",
|
"report_reference_doctype",
|
||||||
|
|
@ -206,13 +207,21 @@
|
||||||
"fieldname": "show_form_tour",
|
"fieldname": "show_form_tour",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Show Form Tour"
|
"label": "Show Form Tour"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "show_form_tour",
|
||||||
|
"fieldname": "form_tour",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Form Tour",
|
||||||
|
"options": "Form Tour"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-30 14:54:06.646513",
|
"modified": "2021-12-02 10:56:04.448580",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Desk",
|
"module": "Desk",
|
||||||
"name": "Onboarding Step",
|
"name": "Onboarding Step",
|
||||||
|
"naming_rule": "Set by user",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
# Copyright (c) 2021, Frappe Technologies and contributors
|
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||||
# License: MIT. See LICENSE
|
# License: MIT. See LICENSE
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.deferred_insert import deferred_insert as _deferred_insert
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
class RouteHistory(Document):
|
class RouteHistory(Document):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -35,3 +39,16 @@ def flush_old_route_records():
|
||||||
"modified": ("<=", last_record_to_keep[0].modified),
|
"modified": ("<=", last_record_to_keep[0].modified),
|
||||||
"user": user
|
"user": user
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def deferred_insert(routes):
|
||||||
|
routes = [
|
||||||
|
{
|
||||||
|
"user": frappe.session.user,
|
||||||
|
"route": route.get("route"),
|
||||||
|
"creation": route.get("creation"),
|
||||||
|
}
|
||||||
|
for route in frappe.parse_json(routes)
|
||||||
|
]
|
||||||
|
|
||||||
|
_deferred_insert("Route History", json.dumps(routes))
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
# License: MIT. See LICENSE
|
# License: MIT. See LICENSE
|
||||||
import json
|
import json
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
import itertools
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import frappe.desk.form.load
|
import frappe.desk.form.load
|
||||||
|
|
@ -12,69 +14,296 @@ from frappe.modules import load_doctype_module
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_submitted_linked_docs(doctype, name, docs=None, visited=None):
|
def get_submitted_linked_docs(doctype: str, name: str) -> List[tuple]:
|
||||||
|
""" Get all the nested submitted documents those are present in referencing tables (dependent tables).
|
||||||
|
|
||||||
|
:param doctype: Document type
|
||||||
|
:param name: Name of the document
|
||||||
|
|
||||||
|
Usecase:
|
||||||
|
* User should be able to cancel the linked documents along with the one user trying to cancel.
|
||||||
|
|
||||||
|
Case1: If document sd1-n1 (document name n1 from sumittable doctype sd1) is linked to sd2-n2 and sd2-n2 is linked to sd3-n3,
|
||||||
|
Getting submittable linked docs of `sd1-n1`should give both sd2-n2 and sd3-n3.
|
||||||
|
Case2: If document sd1-n1 (document name n1 from sumittable doctype sd1) is linked to d2-n2 and d2-n2 is linked to sd3-n3,
|
||||||
|
Getting submittable linked docs of `sd1-n1`should give None. (because d2-n2 is not a submittable doctype)
|
||||||
|
Case3: If document sd1-n1 (document name n1 from submittable doctype sd1) is linked to d2-n2 & sd2-n2. d2-n2 is linked to sd3-n3.
|
||||||
|
Getting submittable linked docs of `sd1-n1`should give sd2-n2.
|
||||||
|
|
||||||
|
Logic:
|
||||||
|
-----
|
||||||
|
1. We can find linked documents only if we know how the doctypes are related.
|
||||||
|
2. As we need only submittable documents, we can limit doctype relations search to submittable doctypes by
|
||||||
|
finding the relationships(Foreign key references) across submittable doctypes.
|
||||||
|
3. Searching for links is going to be a tree like structure where at every level,
|
||||||
|
you will be finding documents using parent document and parent document links.
|
||||||
"""
|
"""
|
||||||
Get all nested submitted linked doctype linkinfo
|
tree = SubmittableDocumentTree(doctype, name)
|
||||||
|
visited_documents = tree.get_all_children()
|
||||||
|
docs = []
|
||||||
|
|
||||||
Arguments:
|
for dt, names in visited_documents.items():
|
||||||
doctype (str) - The doctype for which get all linked doctypes
|
docs.extend([{'doctype': dt, 'name': name, 'docstatus': 1} for name in names])
|
||||||
name (str) - The docname for which get all linked doctypes
|
|
||||||
|
|
||||||
Keyword Arguments:
|
|
||||||
docs (list of dict) - (Optional) Get list of dictionary for linked doctype.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict - Return list of documents and link count
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not docs:
|
|
||||||
docs = []
|
|
||||||
|
|
||||||
if not visited:
|
|
||||||
visited = {}
|
|
||||||
|
|
||||||
if doctype not in visited:
|
|
||||||
visited[doctype] = []
|
|
||||||
|
|
||||||
if name in visited[doctype]:
|
|
||||||
return
|
|
||||||
|
|
||||||
linkinfo = get_linked_doctypes(doctype)
|
|
||||||
linked_docs = get_linked_docs(doctype, name, linkinfo)
|
|
||||||
|
|
||||||
link_count = 0
|
|
||||||
visited[doctype].append(name)
|
|
||||||
|
|
||||||
for link_doctype, link_names in linked_docs.items():
|
|
||||||
|
|
||||||
for link in link_names:
|
|
||||||
if link['name'] == name:
|
|
||||||
continue
|
|
||||||
|
|
||||||
docinfo = link.update({"doctype": link_doctype})
|
|
||||||
validated_doc = validate_linked_doc(docinfo)
|
|
||||||
|
|
||||||
if not validated_doc:
|
|
||||||
continue
|
|
||||||
|
|
||||||
link_count += 1
|
|
||||||
|
|
||||||
links = get_submitted_linked_docs(link_doctype, link.name, docs, visited)
|
|
||||||
if links:
|
|
||||||
docs.append({
|
|
||||||
"doctype": link_doctype,
|
|
||||||
"name": link.name,
|
|
||||||
"docstatus": link.docstatus,
|
|
||||||
"link_count": links.get("count")
|
|
||||||
})
|
|
||||||
|
|
||||||
# sort linked documents by ascending number of links
|
|
||||||
docs.sort(key=lambda doc: doc.get("link_count"))
|
|
||||||
return {
|
return {
|
||||||
"docs": docs,
|
"docs": docs,
|
||||||
"count": link_count
|
"count": len(docs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SubmittableDocumentTree:
|
||||||
|
def __init__(self, doctype: str, name: str):
|
||||||
|
"""Construct a tree for the submitable linked documents.
|
||||||
|
|
||||||
|
* Node has properties like doctype and docnames. Represented as Node(doctype, docnames).
|
||||||
|
* Nodes are linked by doctype relationships like table, link and dynamic links.
|
||||||
|
* Node is referenced(linked) by many other documents and those are the child nodes.
|
||||||
|
|
||||||
|
NOTE: child document is a property of child node (not same as Frappe child docs of a table field).
|
||||||
|
"""
|
||||||
|
self.root_doctype = doctype
|
||||||
|
self.root_docname = name
|
||||||
|
|
||||||
|
# Documents those are yet to be visited for linked documents.
|
||||||
|
self.to_be_visited_documents = {doctype: [name]}
|
||||||
|
self.visited_documents = defaultdict(list)
|
||||||
|
|
||||||
|
self._submittable_doctypes = None # All submittable doctypes in the system
|
||||||
|
self._references_across_doctypes = None # doctype wise links/references
|
||||||
|
|
||||||
|
def get_all_children(self):
|
||||||
|
"""Get all nodes of a tree except the root node (all the nested submitted
|
||||||
|
documents those are present in referencing tables (dependent tables).
|
||||||
|
"""
|
||||||
|
while self.to_be_visited_documents:
|
||||||
|
next_level_children = defaultdict(list)
|
||||||
|
for parent_dt in list(self.to_be_visited_documents):
|
||||||
|
parent_docs = self.to_be_visited_documents.get(parent_dt)
|
||||||
|
if not parent_docs:
|
||||||
|
del self.to_be_visited_documents[parent_dt]
|
||||||
|
continue
|
||||||
|
|
||||||
|
child_docs = self.get_next_level_children(parent_dt, parent_docs)
|
||||||
|
self.visited_documents[parent_dt].extend(parent_docs)
|
||||||
|
for linked_dt, linked_names in child_docs.items():
|
||||||
|
not_visited_child_docs = set(linked_names) - set(self.visited_documents.get(linked_dt, []))
|
||||||
|
next_level_children[linked_dt].extend(not_visited_child_docs)
|
||||||
|
|
||||||
|
self.to_be_visited_documents = next_level_children
|
||||||
|
|
||||||
|
# Remove root node from visited documents
|
||||||
|
if self.root_docname in self.visited_documents.get(self.root_doctype, []):
|
||||||
|
self.visited_documents[self.root_doctype].remove(self.root_docname)
|
||||||
|
|
||||||
|
return self.visited_documents
|
||||||
|
|
||||||
|
def get_next_level_children(self, parent_dt, parent_names):
|
||||||
|
"""Get immediate children of a Node(parent_dt, parent_names)
|
||||||
|
"""
|
||||||
|
referencing_fields = self.get_doctype_references(parent_dt)
|
||||||
|
|
||||||
|
child_docs = defaultdict(list)
|
||||||
|
for field in referencing_fields:
|
||||||
|
links = get_referencing_documents(parent_dt, parent_names.copy(), field, get_parent_if_child_table_doc=True,
|
||||||
|
parent_filters=[('docstatus', '=', 1)], allowed_parents=self.get_link_sources()) or {}
|
||||||
|
for dt, names in links.items():
|
||||||
|
child_docs[dt].extend(names)
|
||||||
|
return child_docs
|
||||||
|
|
||||||
|
def get_doctype_references(self, doctype):
|
||||||
|
"""Get references for a given document.
|
||||||
|
"""
|
||||||
|
if self._references_across_doctypes is None:
|
||||||
|
get_links_to = self.get_document_sources()
|
||||||
|
limit_link_doctypes = self.get_link_sources()
|
||||||
|
self._references_across_doctypes = get_references_across_doctypes(
|
||||||
|
get_links_to, limit_link_doctypes)
|
||||||
|
return self._references_across_doctypes.get(doctype, [])
|
||||||
|
|
||||||
|
def get_document_sources(self):
|
||||||
|
"""Returns list of doctypes from where we access submittable documents.
|
||||||
|
"""
|
||||||
|
return list(set(self.get_link_sources() + [self.root_doctype]))
|
||||||
|
|
||||||
|
def get_link_sources(self):
|
||||||
|
"""limit doctype links to these doctypes.
|
||||||
|
"""
|
||||||
|
return list(set(self.get_submittable_doctypes()) - set(get_exempted_doctypes() or []))
|
||||||
|
|
||||||
|
def get_submittable_doctypes(self) -> List[str]:
|
||||||
|
"""Returns list of submittable doctypes.
|
||||||
|
"""
|
||||||
|
if not self._submittable_doctypes:
|
||||||
|
self._submittable_doctypes = frappe.db.get_all('DocType', {'is_submittable': 1}, pluck='name')
|
||||||
|
return self._submittable_doctypes
|
||||||
|
|
||||||
|
|
||||||
|
def get_child_tables_of_doctypes(doctypes: List[str]=None):
|
||||||
|
"""Returns child tables by doctype.
|
||||||
|
"""
|
||||||
|
filters=[['fieldtype','=', 'Table']]
|
||||||
|
filters_for_docfield = filters
|
||||||
|
filters_for_customfield = filters
|
||||||
|
|
||||||
|
if doctypes:
|
||||||
|
filters_for_docfield = filters + [['parent', 'in', tuple(doctypes)]]
|
||||||
|
filters_for_customfield = filters + [['dt', 'in', tuple(doctypes)]]
|
||||||
|
|
||||||
|
links = frappe.get_all("DocField",
|
||||||
|
fields=["parent", "fieldname", "options as child_table"],
|
||||||
|
filters=filters_for_docfield,
|
||||||
|
as_list=1)
|
||||||
|
|
||||||
|
links+= frappe.get_all("Custom Field",
|
||||||
|
fields=["dt as parent", "fieldname", "options as child_table"],
|
||||||
|
filters=filters_for_customfield,
|
||||||
|
as_list=1)
|
||||||
|
|
||||||
|
child_tables_by_doctype = defaultdict(list)
|
||||||
|
for doctype, fieldname, child_table in links:
|
||||||
|
child_tables_by_doctype[doctype].append(
|
||||||
|
{'doctype': doctype, 'fieldname': fieldname, 'child_table': child_table})
|
||||||
|
return child_tables_by_doctype
|
||||||
|
|
||||||
|
|
||||||
|
def get_references_across_doctypes(to_doctypes: List[str]=None, limit_link_doctypes: List[str]=None) -> List:
|
||||||
|
"""Find doctype wise foreign key references.
|
||||||
|
|
||||||
|
:param to_doctypes: Get links of these doctypes.
|
||||||
|
:param limit_link_doctypes: limit links to these doctypes.
|
||||||
|
|
||||||
|
* Include child table, link and dynamic link references.
|
||||||
|
"""
|
||||||
|
if limit_link_doctypes:
|
||||||
|
child_tables_by_doctype = get_child_tables_of_doctypes(limit_link_doctypes)
|
||||||
|
all_child_tables = [each['child_table'] for each in itertools.chain(*child_tables_by_doctype.values())]
|
||||||
|
limit_link_doctypes = limit_link_doctypes + all_child_tables
|
||||||
|
else:
|
||||||
|
child_tables_by_doctype = get_child_tables_of_doctypes()
|
||||||
|
all_child_tables = [each['child_table'] for each in itertools.chain(*child_tables_by_doctype.values())]
|
||||||
|
|
||||||
|
references_by_link_fields = get_references_across_doctypes_by_link_field(to_doctypes, limit_link_doctypes)
|
||||||
|
references_by_dlink_fields = get_references_across_doctypes_by_dynamic_link_field(to_doctypes, limit_link_doctypes)
|
||||||
|
|
||||||
|
references = references_by_link_fields.copy()
|
||||||
|
for k, v in references_by_dlink_fields.items():
|
||||||
|
references.setdefault(k, []).extend(v)
|
||||||
|
|
||||||
|
for doctype, links in references.items():
|
||||||
|
for link in links:
|
||||||
|
link['is_child'] = (link['doctype'] in all_child_tables)
|
||||||
|
return references
|
||||||
|
|
||||||
|
|
||||||
|
def get_references_across_doctypes_by_link_field(to_doctypes: List[str]=None, limit_link_doctypes: List[str]=None):
|
||||||
|
"""Find doctype wise foreign key references based on link fields.
|
||||||
|
|
||||||
|
:param to_doctypes: Get links to these doctypes.
|
||||||
|
:param limit_link_doctypes: limit links to these doctypes.
|
||||||
|
"""
|
||||||
|
filters=[['fieldtype','=', 'Link']]
|
||||||
|
|
||||||
|
if to_doctypes:
|
||||||
|
filters += [['options', 'in', tuple(to_doctypes)]]
|
||||||
|
|
||||||
|
filters_for_docfield = filters[:]
|
||||||
|
filters_for_customfield = filters[:]
|
||||||
|
|
||||||
|
if limit_link_doctypes:
|
||||||
|
filters_for_docfield += [['parent', 'in', tuple(limit_link_doctypes)]]
|
||||||
|
filters_for_customfield += [['dt', 'in', tuple(limit_link_doctypes)]]
|
||||||
|
|
||||||
|
links = frappe.get_all("DocField",
|
||||||
|
fields=["parent", "fieldname", "options as linked_to"],
|
||||||
|
filters=filters_for_docfield,
|
||||||
|
as_list=1)
|
||||||
|
|
||||||
|
links+= frappe.get_all("Custom Field",
|
||||||
|
fields=["dt as parent", "fieldname", "options as linked_to"],
|
||||||
|
filters=filters_for_customfield,
|
||||||
|
as_list=1)
|
||||||
|
|
||||||
|
links_by_doctype = defaultdict(list)
|
||||||
|
for doctype, fieldname, linked_to in links:
|
||||||
|
links_by_doctype[linked_to].append({'doctype': doctype, 'fieldname': fieldname})
|
||||||
|
return links_by_doctype
|
||||||
|
|
||||||
|
|
||||||
|
def get_references_across_doctypes_by_dynamic_link_field(to_doctypes: List[str]=None, limit_link_doctypes: List[str]=None):
|
||||||
|
"""Find doctype wise foreign key references based on dynamic link fields.
|
||||||
|
|
||||||
|
:param to_doctypes: Get links to these doctypes.
|
||||||
|
:param limit_link_doctypes: limit links to these doctypes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
filters=[['fieldtype','=', 'Dynamic Link']]
|
||||||
|
|
||||||
|
filters_for_docfield = filters[:]
|
||||||
|
filters_for_customfield = filters[:]
|
||||||
|
|
||||||
|
if limit_link_doctypes:
|
||||||
|
filters_for_docfield += [['parent', 'in', tuple(limit_link_doctypes)]]
|
||||||
|
filters_for_customfield += [['dt', 'in', tuple(limit_link_doctypes)]]
|
||||||
|
|
||||||
|
# find dynamic links of parents
|
||||||
|
links = frappe.get_all("DocField",
|
||||||
|
fields=["parent as doctype", "fieldname", "options as doctype_fieldname"],
|
||||||
|
filters=filters_for_docfield,
|
||||||
|
as_list=1)
|
||||||
|
|
||||||
|
links += frappe.get_all("Custom Field",
|
||||||
|
fields=["dt as doctype", "fieldname", "options as doctype_fieldname"],
|
||||||
|
filters=filters_for_customfield,
|
||||||
|
as_list=1)
|
||||||
|
|
||||||
|
links_by_doctype = defaultdict(list)
|
||||||
|
for doctype, fieldname, doctype_fieldname in links:
|
||||||
|
try:
|
||||||
|
filters = [[doctype_fieldname, 'in', to_doctypes]] if to_doctypes else []
|
||||||
|
for linked_to in frappe.db.get_all(doctype, pluck=doctype_fieldname, filters = filters, distinct=1):
|
||||||
|
if linked_to:
|
||||||
|
links_by_doctype[linked_to].append({'doctype': doctype, 'fieldname': fieldname, 'doctype_fieldname': doctype_fieldname})
|
||||||
|
except frappe.db.ProgrammingError:
|
||||||
|
# TODO: FIXME
|
||||||
|
continue
|
||||||
|
return links_by_doctype
|
||||||
|
|
||||||
|
def get_referencing_documents(reference_doctype: str, reference_names: List[str],
|
||||||
|
link_info: dict, get_parent_if_child_table_doc: bool=True,
|
||||||
|
parent_filters: List[list]=None, child_filters=None, allowed_parents=None):
|
||||||
|
"""Get linked documents based on link_info.
|
||||||
|
|
||||||
|
:param reference_doctype: reference doctype to find links
|
||||||
|
:param reference_names: reference document names to find links for
|
||||||
|
:param link_info: linking details to get the linked documents
|
||||||
|
Ex: {'doctype': 'Purchase Invoice Advance', 'fieldname': 'reference_name',
|
||||||
|
'doctype_fieldname': 'reference_type', 'is_child': True}
|
||||||
|
:param get_parent_if_child_table_doc: Get parent record incase linked document is a child table record.
|
||||||
|
:param parent_filters: filters to apply on if not a child table.
|
||||||
|
:param child_filters: apply filters if it is a child table.
|
||||||
|
:param allowed_parents: list of parents allowed in case of get_parent_if_child_table_doc
|
||||||
|
is enabled.
|
||||||
|
"""
|
||||||
|
from_table = link_info['doctype']
|
||||||
|
filters = [[link_info['fieldname'], 'in', tuple(reference_names)]]
|
||||||
|
if link_info.get('doctype_fieldname'):
|
||||||
|
filters.append([link_info['doctype_fieldname'], '=', reference_doctype])
|
||||||
|
|
||||||
|
if not link_info.get('is_child'):
|
||||||
|
filters.extend(parent_filters or [])
|
||||||
|
return {from_table: frappe.db.get_all(from_table, filters, pluck='name')}
|
||||||
|
|
||||||
|
|
||||||
|
filters.extend(child_filters or [])
|
||||||
|
res = frappe.db.get_all(from_table, filters = filters, fields = ['name', 'parenttype', 'parent'])
|
||||||
|
documents = defaultdict(list)
|
||||||
|
|
||||||
|
for parent, rows in itertools.groupby(res, key = lambda row: row['parenttype']):
|
||||||
|
if allowed_parents and parent not in allowed_parents:
|
||||||
|
continue
|
||||||
|
filters = (parent_filters or []) + [['name', 'in', tuple([row.parent for row in rows])]]
|
||||||
|
documents[parent].extend(frappe.db.get_all(parent, filters=filters, pluck='name') or [])
|
||||||
|
return documents
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=None):
|
def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=None):
|
||||||
|
|
@ -109,7 +338,6 @@ def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=None):
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if linked document passes all validations, else False
|
bool: True if linked document passes all validations, else False
|
||||||
"""
|
"""
|
||||||
|
|
||||||
#ignore doctype to cancel
|
#ignore doctype to cancel
|
||||||
if docinfo.get("doctype") in (ignore_doctypes_on_cancel_all or []):
|
if docinfo.get("doctype") in (ignore_doctypes_on_cancel_all or []):
|
||||||
return False
|
return False
|
||||||
|
|
@ -132,7 +360,6 @@ def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=None):
|
||||||
|
|
||||||
def get_exempted_doctypes():
|
def get_exempted_doctypes():
|
||||||
""" Get list of doctypes exempted from being auto-cancelled """
|
""" Get list of doctypes exempted from being auto-cancelled """
|
||||||
|
|
||||||
auto_cancel_exempt_doctypes = []
|
auto_cancel_exempt_doctypes = []
|
||||||
for doctypes in frappe.get_hooks('auto_cancel_exempted_doctypes'):
|
for doctypes in frappe.get_hooks('auto_cancel_exempted_doctypes'):
|
||||||
auto_cancel_exempt_doctypes.append(doctypes)
|
auto_cancel_exempt_doctypes.append(doctypes)
|
||||||
|
|
@ -183,11 +410,11 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if link.get("filters"):
|
if link.get("filters"):
|
||||||
ret = frappe.get_list(doctype=dt, fields=fields, filters=link.get("filters"))
|
ret = frappe.get_all(doctype=dt, fields=fields, filters=link.get("filters"))
|
||||||
|
|
||||||
elif link.get("get_parent"):
|
elif link.get("get_parent"):
|
||||||
if me and me.parent and me.parenttype == dt:
|
if me and me.parent and me.parenttype == dt:
|
||||||
ret = frappe.get_list(doctype=dt, fields=fields,
|
ret = frappe.get_all(doctype=dt, fields=fields,
|
||||||
filters=[[dt, "name", '=', me.parent]])
|
filters=[[dt, "name", '=', me.parent]])
|
||||||
else:
|
else:
|
||||||
ret = None
|
ret = None
|
||||||
|
|
@ -199,7 +426,7 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
|
||||||
if link.get("doctype_fieldname"):
|
if link.get("doctype_fieldname"):
|
||||||
filters.append([link.get('child_doctype'), link.get("doctype_fieldname"), "=", doctype])
|
filters.append([link.get('child_doctype'), link.get("doctype_fieldname"), "=", doctype])
|
||||||
|
|
||||||
ret = frappe.get_list(doctype=dt, fields=fields, filters=filters, or_filters=or_filters, distinct=True)
|
ret = frappe.get_all(doctype=dt, fields=fields, filters=filters, or_filters=or_filters, distinct=True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
link_fieldnames = link.get("fieldname")
|
link_fieldnames = link.get("fieldname")
|
||||||
|
|
@ -210,7 +437,7 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
|
||||||
# dynamic link
|
# dynamic link
|
||||||
if link.get("doctype_fieldname"):
|
if link.get("doctype_fieldname"):
|
||||||
filters.append([dt, link.get("doctype_fieldname"), "=", doctype])
|
filters.append([dt, link.get("doctype_fieldname"), "=", doctype])
|
||||||
ret = frappe.get_list(doctype=dt, fields=fields, filters=filters, or_filters=or_filters)
|
ret = frappe.get_all(doctype=dt, fields=fields, filters=filters, or_filters=or_filters)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
ret = None
|
ret = None
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,8 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
if (r.message.status === 'ok') {
|
if (r.message.status === 'ok') {
|
||||||
this.post_setup_success();
|
this.post_setup_success();
|
||||||
|
} else if (r.message.status === 'registered') {
|
||||||
|
this.update_setup_message(__("starting the setup..."));
|
||||||
} else if (r.message.fail !== undefined) {
|
} else if (r.message.fail !== undefined) {
|
||||||
this.abort_setup(r.message.fail);
|
this.abort_setup(r.message.fail);
|
||||||
}
|
}
|
||||||
|
|
@ -238,6 +240,9 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
|
||||||
if (data.fail_msg) {
|
if (data.fail_msg) {
|
||||||
this.abort_setup(data.fail_msg);
|
this.abort_setup(data.fail_msg);
|
||||||
}
|
}
|
||||||
|
if (data.status === 'ok') {
|
||||||
|
this.post_setup_success();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,17 @@ def setup_complete(args):
|
||||||
return {'status': 'ok'}
|
return {'status': 'ok'}
|
||||||
|
|
||||||
args = parse_args(args)
|
args = parse_args(args)
|
||||||
|
|
||||||
stages = get_setup_stages(args)
|
stages = get_setup_stages(args)
|
||||||
|
is_background_task = frappe.conf.get('trigger_site_setup_in_background')
|
||||||
|
|
||||||
|
if is_background_task:
|
||||||
|
process_setup_stages.enqueue(stages=stages, user_input=args, is_background_task=True)
|
||||||
|
return {'status': 'registered'}
|
||||||
|
else:
|
||||||
|
return process_setup_stages(stages, args)
|
||||||
|
|
||||||
|
@frappe.task()
|
||||||
|
def process_setup_stages(stages, user_input, is_background_task=False):
|
||||||
try:
|
try:
|
||||||
frappe.flags.in_setup_wizard = True
|
frappe.flags.in_setup_wizard = True
|
||||||
current_task = None
|
current_task = None
|
||||||
|
|
@ -68,11 +76,16 @@ def setup_complete(args):
|
||||||
current_task = task
|
current_task = task
|
||||||
task.get('fn')(task.get('args'))
|
task.get('fn')(task.get('args'))
|
||||||
except Exception:
|
except Exception:
|
||||||
handle_setup_exception(args)
|
handle_setup_exception(user_input)
|
||||||
return {'status': 'fail', 'fail': current_task.get('fail_msg')}
|
if not is_background_task:
|
||||||
|
return {'status': 'fail', 'fail': current_task.get('fail_msg')}
|
||||||
|
frappe.publish_realtime('setup_task',
|
||||||
|
{'status': 'fail', "fail_msg": current_task.get('fail_msg')}, user=frappe.session.user)
|
||||||
else:
|
else:
|
||||||
run_setup_success(args)
|
run_setup_success(user_input)
|
||||||
return {'status': 'ok'}
|
if not is_background_task:
|
||||||
|
return {'status': 'ok'}
|
||||||
|
frappe.publish_realtime('setup_task', {"status": 'ok'}, user=frappe.session.user)
|
||||||
finally:
|
finally:
|
||||||
frappe.flags.in_setup_wizard = False
|
frappe.flags.in_setup_wizard = False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,21 +17,15 @@ class UserProfile {
|
||||||
show() {
|
show() {
|
||||||
let route = frappe.get_route();
|
let route = frappe.get_route();
|
||||||
this.user_id = route[1] || frappe.session.user;
|
this.user_id = route[1] || frappe.session.user;
|
||||||
|
frappe.dom.freeze(__('Loading user profile') + '...');
|
||||||
//validate if user
|
frappe.db.exists('User', this.user_id).then(exists => {
|
||||||
if (route.length > 1) {
|
frappe.dom.unfreeze();
|
||||||
frappe.dom.freeze(__('Loading user profile') + '...');
|
if (exists) {
|
||||||
frappe.db.exists('User', this.user_id).then(exists => {
|
this.make_user_profile();
|
||||||
frappe.dom.unfreeze();
|
} else {
|
||||||
if (exists) {
|
frappe.msgprint(__('User does not exist'));
|
||||||
this.make_user_profile();
|
}
|
||||||
} else {
|
});
|
||||||
frappe.msgprint(__('User does not exist'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
frappe.set_route('user-profile', frappe.session.user);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
make_user_profile() {
|
make_user_profile() {
|
||||||
|
|
@ -74,8 +68,7 @@ class UserProfile {
|
||||||
primary_action_label: __('Go'),
|
primary_action_label: __('Go'),
|
||||||
primary_action: ({ user }) => {
|
primary_action: ({ user }) => {
|
||||||
dialog.hide();
|
dialog.hide();
|
||||||
this.user_id = user;
|
frappe.set_route('user-profile', user);
|
||||||
this.make_user_profile();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
dialog.show();
|
dialog.show();
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,10 @@
|
||||||
<p><a class="edit-profile-link">{%=__("Edit Profile") %}</a></p>
|
<p><a class="edit-profile-link">{%=__("Edit Profile") %}</a></p>
|
||||||
<p><a class="user-settings-link">{%=__("User Settings") %}</a></p>
|
<p><a class="user-settings-link">{%=__("User Settings") %}</a></p>
|
||||||
<p>
|
<p>
|
||||||
<a class="leaderboard-link" href="#leaderboard/User"
|
<a class="leaderboard-link" href="/app/leaderboard/User"
|
||||||
>{%=__("Leaderboard") %}</a
|
>{%=__("Leaderboard") %}</a
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,15 @@ frappe.ui.form.on("Email Account", {
|
||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
frm.set_df_property("append_to", "only_select", true);
|
frm.set_df_property("append_to", "only_select", true);
|
||||||
frm.set_query("append_to", "frappe.email.doctype.email_account.email_account.get_append_to");
|
frm.set_query("append_to", "frappe.email.doctype.email_account.email_account.get_append_to");
|
||||||
|
frm.set_query("append_to", "imap_folder", function() {
|
||||||
|
return {
|
||||||
|
query: "frappe.email.doctype.email_account.email_account.get_append_to"
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (frm.doc.__islocal) {
|
||||||
|
frm.add_child("imap_folder", {"folder_name": "INBOX"});
|
||||||
|
frm.refresh_field("imap_folder");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
|
|
@ -117,7 +126,7 @@ frappe.ui.form.on("Email Account", {
|
||||||
frm.events.notify_if_unreplied(frm);
|
frm.events.notify_if_unreplied(frm);
|
||||||
frm.events.show_gmail_message_for_less_secure_apps(frm);
|
frm.events.show_gmail_message_for_less_secure_apps(frm);
|
||||||
|
|
||||||
if(frappe.route_flags.delete_user_from_locals && frappe.route_flags.linked_user) {
|
if (frappe.route_flags.delete_user_from_locals && frappe.route_flags.linked_user) {
|
||||||
delete frappe.route_flags.delete_user_from_locals;
|
delete frappe.route_flags.delete_user_from_locals;
|
||||||
delete locals['User'][frappe.route_flags.linked_user];
|
delete locals['User'][frappe.route_flags.linked_user];
|
||||||
}
|
}
|
||||||
|
|
@ -125,7 +134,7 @@ frappe.ui.form.on("Email Account", {
|
||||||
|
|
||||||
show_gmail_message_for_less_secure_apps: function(frm) {
|
show_gmail_message_for_less_secure_apps: function(frm) {
|
||||||
frm.dashboard.clear_headline();
|
frm.dashboard.clear_headline();
|
||||||
if(frm.doc.service==="GMail") {
|
if (frm.doc.service==="GMail") {
|
||||||
frm.dashboard.set_headline_alert('Gmail will only work if you allow access for less secure \
|
frm.dashboard.set_headline_alert('Gmail will only work if you allow access for less secure \
|
||||||
apps in Gmail settings. <a target="_blank" \
|
apps in Gmail settings. <a target="_blank" \
|
||||||
href="https://support.google.com/accounts/answer/6010255?hl=en">Read this for details</a>');
|
href="https://support.google.com/accounts/answer/6010255?hl=en">Read this for details</a>');
|
||||||
|
|
@ -137,8 +146,8 @@ frappe.ui.form.on("Email Account", {
|
||||||
frm.events.update_domain(frm);
|
frm.events.update_domain(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
update_domain: function(frm){
|
update_domain: function(frm) {
|
||||||
if (!frm.doc.email_id && !frm.doc.service){
|
if (!frm.doc.email_id && !frm.doc.service) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,7 +157,7 @@ frappe.ui.form.on("Email Account", {
|
||||||
args: {
|
args: {
|
||||||
"email_id": frm.doc.email_id
|
"email_id": frm.doc.email_id
|
||||||
},
|
},
|
||||||
callback: function (r) {
|
callback: function(r) {
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
frm.events.set_domain_fields(frm, r.message);
|
frm.events.set_domain_fields(frm, r.message);
|
||||||
}
|
}
|
||||||
|
|
@ -157,7 +166,7 @@ frappe.ui.form.on("Email Account", {
|
||||||
},
|
},
|
||||||
|
|
||||||
set_domain_fields: function(frm, args) {
|
set_domain_fields: function(frm, args) {
|
||||||
if(!args){
|
if (!args) {
|
||||||
args = frappe.route_flags.set_domain_values? frappe.route_options: {};
|
args = frappe.route_flags.set_domain_values? frappe.route_options: {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,10 +181,8 @@ frappe.ui.form.on("Email Account", {
|
||||||
email_sync_option: function(frm) {
|
email_sync_option: function(frm) {
|
||||||
// confirm if the ALL sync option is selected
|
// confirm if the ALL sync option is selected
|
||||||
|
|
||||||
if(frm.doc.email_sync_option == "ALL"){
|
if (frm.doc.email_sync_option == "ALL") {
|
||||||
var msg = __("You are selecting Sync Option as ALL, It will resync all \
|
var msg = __("You are selecting Sync Option as ALL, It will resync all read as well as unread message from server. This may also cause the duplication of Communication (emails).");
|
||||||
read as well as unread message from server. This may also cause the duplication\
|
|
||||||
of Communication (emails).");
|
|
||||||
frappe.confirm(msg, null, function() {
|
frappe.confirm(msg, null, function() {
|
||||||
frm.set_value("email_sync_option", "UNSEEN");
|
frm.set_value("email_sync_option", "UNSEEN");
|
||||||
});
|
});
|
||||||
|
|
@ -184,8 +191,7 @@ frappe.ui.form.on("Email Account", {
|
||||||
|
|
||||||
warn_autoreply_on_incoming: function(frm) {
|
warn_autoreply_on_incoming: function(frm) {
|
||||||
if (frm.doc.enable_incoming && frm.doc.enable_auto_reply && frm.doc.__islocal) {
|
if (frm.doc.enable_incoming && frm.doc.enable_auto_reply && frm.doc.__islocal) {
|
||||||
var msg = __("Enabling auto reply on an incoming email account will send automated replies \
|
var msg = __("Enabling auto reply on an incoming email account will send automated replies to all the synchronized emails. Do you wish to continue?");
|
||||||
to all the synchronized emails. Do you wish to continue?");
|
|
||||||
frappe.confirm(msg, null, function() {
|
frappe.confirm(msg, null, function() {
|
||||||
frm.set_value("enable_auto_reply", 0);
|
frm.set_value("enable_auto_reply", 0);
|
||||||
frappe.show_alert({message: __("Disabled Auto Reply"), indicator: "blue"});
|
frappe.show_alert({message: __("Disabled Auto Reply"), indicator: "blue"});
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@
|
||||||
"attachment_limit",
|
"attachment_limit",
|
||||||
"email_sync_option",
|
"email_sync_option",
|
||||||
"initial_sync_count",
|
"initial_sync_count",
|
||||||
|
"section_break_25",
|
||||||
|
"imap_folder",
|
||||||
"section_break_12",
|
"section_break_12",
|
||||||
"append_emails_to_sent_folder",
|
"append_emails_to_sent_folder",
|
||||||
"append_to",
|
"append_to",
|
||||||
|
|
@ -204,7 +206,7 @@
|
||||||
"label": "Attachment Limit (MB)"
|
"label": "Attachment Limit (MB)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "enable_incoming",
|
"depends_on": "eval: doc.enable_incoming && !doc.use_imap",
|
||||||
"description": "Append as communication against this DocType (must have fields, \"Status\", \"Subject\")",
|
"description": "Append as communication against this DocType (must have fields, \"Status\", \"Subject\")",
|
||||||
"fieldname": "append_to",
|
"fieldname": "append_to",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
|
@ -562,15 +564,28 @@
|
||||||
"fieldname": "account_section",
|
"fieldname": "account_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Account"
|
"label": "Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.use_imap && doc.enable_incoming",
|
||||||
|
"fieldname": "imap_folder",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "IMAP Folder",
|
||||||
|
"options": "IMAP Folder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_25",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "IMAP Details"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-inbox",
|
"icon": "fa fa-inbox",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-21 16:44:25.728637",
|
"modified": "2021-11-30 09:03:25.728637",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Email",
|
"module": "Email",
|
||||||
"name": "Email Account",
|
"name": "Email Account",
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,10 @@ class EmailAccount(Document):
|
||||||
else:
|
else:
|
||||||
self.login_id = None
|
self.login_id = None
|
||||||
|
|
||||||
|
# validate the imap settings
|
||||||
|
if self.enable_incoming and self.use_imap and len(self.imap_folder) <= 0:
|
||||||
|
frappe.throw(_("You need to set one IMAP folder for {0}").format(frappe.bold(self.email_id)))
|
||||||
|
|
||||||
duplicate_email_account = frappe.get_all("Email Account", filters={
|
duplicate_email_account = frappe.get_all("Email Account", filters={
|
||||||
"email_id": self.email_id,
|
"email_id": self.email_id,
|
||||||
"name": ("!=", self.name)
|
"name": ("!=", self.name)
|
||||||
|
|
@ -100,10 +104,11 @@ class EmailAccount(Document):
|
||||||
for e in self.get_unreplied_notification_emails():
|
for e in self.get_unreplied_notification_emails():
|
||||||
validate_email_address(e, True)
|
validate_email_address(e, True)
|
||||||
|
|
||||||
if self.enable_incoming and self.append_to:
|
for folder in self.imap_folder:
|
||||||
valid_doctypes = [d[0] for d in get_append_to()]
|
if self.enable_incoming and folder.append_to:
|
||||||
if self.append_to not in valid_doctypes:
|
valid_doctypes = [d[0] for d in get_append_to()]
|
||||||
frappe.throw(_("Append To can be one of {0}").format(comma_or(valid_doctypes)))
|
if folder.append_to not in valid_doctypes:
|
||||||
|
frappe.throw(_("Append To can be one of {0}").format(comma_or(valid_doctypes)))
|
||||||
|
|
||||||
def validate_smtp_conn(self):
|
def validate_smtp_conn(self):
|
||||||
if not self.smtp_server:
|
if not self.smtp_server:
|
||||||
|
|
@ -177,13 +182,13 @@ class EmailAccount(Document):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
args = frappe._dict({
|
args = frappe._dict({
|
||||||
|
"email_account_name": self.email_account_name,
|
||||||
"email_account": self.name,
|
"email_account": self.name,
|
||||||
"host": self.email_server,
|
"host": self.email_server,
|
||||||
"use_ssl": self.use_ssl,
|
"use_ssl": self.use_ssl,
|
||||||
"username": getattr(self, "login_id", None) or self.email_id,
|
"username": getattr(self, "login_id", None) or self.email_id,
|
||||||
"use_imap": self.use_imap,
|
"use_imap": self.use_imap,
|
||||||
"email_sync_rule": email_sync_rule,
|
"email_sync_rule": email_sync_rule,
|
||||||
"uid_validity": self.uidvalidity,
|
|
||||||
"incoming_port": get_port(self),
|
"incoming_port": get_port(self),
|
||||||
"initial_sync_count": self.initial_sync_count or 100
|
"initial_sync_count": self.initial_sync_count or 100
|
||||||
})
|
})
|
||||||
|
|
@ -457,6 +462,14 @@ class EmailAccount(Document):
|
||||||
"""retrive and return inbound mails.
|
"""retrive and return inbound mails.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
mails = []
|
||||||
|
|
||||||
|
def process_mail(messages):
|
||||||
|
for index, message in enumerate(messages.get("latest_messages", [])):
|
||||||
|
uid = messages['uid_list'][index] if messages.get('uid_list') else None
|
||||||
|
seen_status = 1 if messages.get('seen_status', {}).get(uid) == 'SEEN' else 0
|
||||||
|
mails.append(InboundMail(message, self, uid, seen_status))
|
||||||
|
|
||||||
if frappe.local.flags.in_test:
|
if frappe.local.flags.in_test:
|
||||||
return [InboundMail(msg, self) for msg in test_mails or []]
|
return [InboundMail(msg, self) for msg in test_mails or []]
|
||||||
|
|
||||||
|
|
@ -466,17 +479,23 @@ class EmailAccount(Document):
|
||||||
email_sync_rule = self.build_email_sync_rule()
|
email_sync_rule = self.build_email_sync_rule()
|
||||||
try:
|
try:
|
||||||
email_server = self.get_incoming_server(in_receive=True, email_sync_rule=email_sync_rule)
|
email_server = self.get_incoming_server(in_receive=True, email_sync_rule=email_sync_rule)
|
||||||
messages = email_server.get_messages() or {}
|
if self.use_imap:
|
||||||
|
# process all given imap folder
|
||||||
|
for folder in self.imap_folder:
|
||||||
|
email_server.select_imap_folder(folder.folder_name)
|
||||||
|
email_server.settings['uid_validity'] = folder.uidvalidity
|
||||||
|
messages = email_server.get_messages(folder=folder.folder_name) or {}
|
||||||
|
process_mail(messages)
|
||||||
|
else:
|
||||||
|
# process the pop3 account
|
||||||
|
messages = email_server.get_messages() or {}
|
||||||
|
process_mail(messages)
|
||||||
|
# close connection to mailserver
|
||||||
|
email_server.logout()
|
||||||
except Exception:
|
except Exception:
|
||||||
frappe.log_error(title=_("Error while connecting to email account {0}").format(self.name))
|
frappe.log_error(title=_("Error while connecting to email account {0}").format(self.name))
|
||||||
return []
|
return []
|
||||||
|
|
||||||
mails = []
|
|
||||||
for index, message in enumerate(messages.get("latest_messages", [])):
|
|
||||||
uid = messages['uid_list'][index] if messages.get('uid_list') else None
|
|
||||||
seen_status = 1 if messages.get('seen_status', {}).get(uid)=='SEEN' else 0
|
|
||||||
mails.append(InboundMail(message, self, uid, seen_status))
|
|
||||||
|
|
||||||
return mails
|
return mails
|
||||||
|
|
||||||
def handle_bad_emails(self, uid, raw, reason):
|
def handle_bad_emails(self, uid, raw, reason):
|
||||||
|
|
@ -530,7 +549,11 @@ class EmailAccount(Document):
|
||||||
|
|
||||||
def on_trash(self):
|
def on_trash(self):
|
||||||
"""Clear communications where email account is linked"""
|
"""Clear communications where email account is linked"""
|
||||||
frappe.db.sql("update `tabCommunication` set email_account='' where email_account=%s", self.name)
|
Communication = frappe.qb.DocType("Communication")
|
||||||
|
frappe.qb.update(Communication) \
|
||||||
|
.set(Communication.email_account, "") \
|
||||||
|
.where(Communication.email_account == self.name).run()
|
||||||
|
|
||||||
remove_user_email_inbox(email_account=self.name)
|
remove_user_email_inbox(email_account=self.name)
|
||||||
|
|
||||||
def after_rename(self, old, new, merge=False):
|
def after_rename(self, old, new, merge=False):
|
||||||
|
|
@ -547,23 +570,26 @@ class EmailAccount(Document):
|
||||||
else:
|
else:
|
||||||
return self.email_sync_option or "UNSEEN"
|
return self.email_sync_option or "UNSEEN"
|
||||||
|
|
||||||
def mark_emails_as_read_unread(self):
|
def mark_emails_as_read_unread(self, email_server=None, folder_name="INBOX"):
|
||||||
""" mark Email Flag Queue of self.email_account mails as read"""
|
""" mark Email Flag Queue of self.email_account mails as read"""
|
||||||
|
|
||||||
if not self.use_imap:
|
if not self.use_imap:
|
||||||
return
|
return
|
||||||
|
|
||||||
flags = frappe.db.sql("""select name, communication, uid, action from
|
EmailFlagQ = frappe.qb.DocType("Email Flag Queue")
|
||||||
`tabEmail Flag Queue` where is_completed=0 and email_account={email_account}
|
flags = (
|
||||||
""".format(email_account=frappe.db.escape(self.name)), as_dict=True)
|
frappe.qb.from_(EmailFlagQ)
|
||||||
|
.select(EmailFlagQ.name, EmailFlagQ.communication, EmailFlagQ.uid, EmailFlagQ.action)
|
||||||
|
.where(EmailFlagQ.is_completed == 0)
|
||||||
|
.where(EmailFlagQ.email_account == frappe.db.escape(self.name))
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
uid_list = { flag.get("uid", None): flag.get("action", "Read") for flag in flags }
|
uid_list = { flag.get("uid", None): flag.get("action", "Read") for flag in flags }
|
||||||
if flags and uid_list:
|
if flags and uid_list:
|
||||||
email_server = self.get_incoming_server()
|
if not email_server:
|
||||||
|
email_server = self.get_incoming_server()
|
||||||
if not email_server:
|
if not email_server:
|
||||||
return
|
return
|
||||||
|
email_server.update_flag(folder_name, uid_list=uid_list)
|
||||||
email_server.update_flag(uid_list=uid_list)
|
|
||||||
|
|
||||||
# mark communication as read
|
# mark communication as read
|
||||||
docnames = ",".join("'%s'"%flag.get("communication") for flag in flags \
|
docnames = ",".join("'%s'"%flag.get("communication") for flag in flags \
|
||||||
|
|
@ -576,16 +602,20 @@ class EmailAccount(Document):
|
||||||
self.set_communication_seen_status(docnames, seen=0)
|
self.set_communication_seen_status(docnames, seen=0)
|
||||||
|
|
||||||
docnames = ",".join([ "'%s'"%flag.get("name") for flag in flags ])
|
docnames = ",".join([ "'%s'"%flag.get("name") for flag in flags ])
|
||||||
frappe.db.sql(""" update `tabEmail Flag Queue` set is_completed=1
|
|
||||||
where name in ({docnames})""".format(docnames=docnames))
|
EmailFlagQueue = frappe.qb.DocType("Email Flag Queue")
|
||||||
|
frappe.qb.update(EmailFlagQueue) \
|
||||||
|
.set(EmailFlagQueue.is_completed, 1) \
|
||||||
|
.where(EmailFlagQueue.name.isin(docnames)).run()
|
||||||
|
|
||||||
def set_communication_seen_status(self, docnames, seen=0):
|
def set_communication_seen_status(self, docnames, seen=0):
|
||||||
""" mark Email Flag Queue of self.email_account mails as read"""
|
""" mark Email Flag Queue of self.email_account mails as read"""
|
||||||
if not docnames:
|
if not docnames:
|
||||||
return
|
return
|
||||||
|
Communication = frappe.qb.from_("Communication")
|
||||||
frappe.db.sql(""" update `tabCommunication` set seen={seen}
|
frappe.qb.update(Communication) \
|
||||||
where name in ({docnames})""".format(docnames=docnames, seen=seen))
|
.set(Communication.seen == seen) \
|
||||||
|
.where(Communication.name.isin(docnames)).run()
|
||||||
|
|
||||||
def check_automatic_linking_email_account(self):
|
def check_automatic_linking_email_account(self):
|
||||||
if self.enable_automatic_linking:
|
if self.enable_automatic_linking:
|
||||||
|
|
@ -651,15 +681,19 @@ def test_internet(host="8.8.8.8", port=53, timeout=3):
|
||||||
def notify_unreplied():
|
def notify_unreplied():
|
||||||
"""Sends email notifications if there are unreplied Communications
|
"""Sends email notifications if there are unreplied Communications
|
||||||
and `notify_if_unreplied` is set as true."""
|
and `notify_if_unreplied` is set as true."""
|
||||||
|
|
||||||
for email_account in frappe.get_all("Email Account", "name", filters={"enable_incoming": 1, "notify_if_unreplied": 1}):
|
for email_account in frappe.get_all("Email Account", "name", filters={"enable_incoming": 1, "notify_if_unreplied": 1}):
|
||||||
email_account = frappe.get_doc("Email Account", email_account.name)
|
email_account = frappe.get_doc("Email Account", email_account.name)
|
||||||
if email_account.append_to:
|
|
||||||
|
|
||||||
|
if email_account.use_imap:
|
||||||
|
append_to = [folder.get("append_to") for folder in email_account.imap_folder]
|
||||||
|
else:
|
||||||
|
append_to = email_account.append_to
|
||||||
|
|
||||||
|
if append_to:
|
||||||
# get open communications younger than x mins, for given doctype
|
# get open communications younger than x mins, for given doctype
|
||||||
for comm in frappe.get_all("Communication", "name", filters=[
|
for comm in frappe.get_all("Communication", "name", filters=[
|
||||||
{"sent_or_received": "Received"},
|
{"sent_or_received": "Received"},
|
||||||
{"reference_doctype": email_account.append_to},
|
{"reference_doctype": ("in", append_to)},
|
||||||
{"unread_notification_sent": 0},
|
{"unread_notification_sent": 0},
|
||||||
{"email_account":email_account.name},
|
{"email_account":email_account.name},
|
||||||
{"creation": ("<", datetime.now() - timedelta(seconds = (email_account.unreplied_for_mins or 30) * 60))},
|
{"creation": ("<", datetime.now() - timedelta(seconds = (email_account.unreplied_for_mins or 30) * 60))},
|
||||||
|
|
@ -702,9 +736,6 @@ def pull_from_email_account(email_account):
|
||||||
email_account = frappe.get_doc("Email Account", email_account)
|
email_account = frappe.get_doc("Email Account", email_account)
|
||||||
email_account.receive()
|
email_account.receive()
|
||||||
|
|
||||||
# mark Email Flag Queue mail as read
|
|
||||||
email_account.mark_emails_as_read_unread()
|
|
||||||
|
|
||||||
def get_max_email_uid(email_account):
|
def get_max_email_uid(email_account):
|
||||||
# get maximum uid of emails
|
# get maximum uid of emails
|
||||||
max_uid = 1
|
max_uid = 1
|
||||||
|
|
@ -761,12 +792,12 @@ def setup_user_email_inbox(email_account, awaiting_password, email_id, enable_ou
|
||||||
update_user_email_settings = True
|
update_user_email_settings = True
|
||||||
|
|
||||||
if update_user_email_settings:
|
if update_user_email_settings:
|
||||||
frappe.db.sql("""UPDATE `tabUser Email` SET awaiting_password = %(awaiting_password)s,
|
UserEmail = frappe.qb.DocType("User Email")
|
||||||
enable_outgoing = %(enable_outgoing)s WHERE email_account = %(email_account)s""", {
|
frappe.qb.update(UserEmail) \
|
||||||
"email_account": email_account,
|
.set(UserEmail.awaiting_password, (awaiting_password or 0)) \
|
||||||
"enable_outgoing": enable_outgoing,
|
.set(UserEmail.enable_outgoing, enable_outgoing) \
|
||||||
"awaiting_password": awaiting_password or 0
|
.where(UserEmail.email_account == email_account).run()
|
||||||
})
|
|
||||||
else:
|
else:
|
||||||
users = " and ".join([frappe.bold(user.get("name")) for user in user_names])
|
users = " and ".join([frappe.bold(user.get("name")) for user in user_names])
|
||||||
frappe.msgprint(_("Enabled email inbox for user {0}").format(users))
|
frappe.msgprint(_("Enabled email inbox for user {0}").format(users))
|
||||||
|
|
@ -800,4 +831,4 @@ def set_email_password(email_account, user, password):
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ class TestEmailAccount(unittest.TestCase):
|
||||||
email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
|
email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
|
||||||
email_account.db_set("enable_incoming", 1)
|
email_account.db_set("enable_incoming", 1)
|
||||||
email_account.db_set("enable_auto_reply", 1)
|
email_account.db_set("enable_auto_reply", 1)
|
||||||
|
email_account.db_set("use_imap", 1)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
|
|
@ -229,6 +230,22 @@ class TestEmailAccount(unittest.TestCase):
|
||||||
email_account.handle_bad_emails(uid=-1, raw=mail_content, reason="Testing")
|
email_account.handle_bad_emails(uid=-1, raw=mail_content, reason="Testing")
|
||||||
self.assertTrue(frappe.db.get_value("Unhandled Email", {'message_id': message_id}))
|
self.assertTrue(frappe.db.get_value("Unhandled Email", {'message_id': message_id}))
|
||||||
|
|
||||||
|
def test_imap_folder(self):
|
||||||
|
# assert tests if imap_folder >= 1 and imap is checked
|
||||||
|
email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
|
||||||
|
|
||||||
|
self.assertTrue(email_account.use_imap)
|
||||||
|
self.assertTrue(email_account.enable_incoming)
|
||||||
|
self.assertTrue(len(email_account.imap_folder) > 0)
|
||||||
|
|
||||||
|
def test_imap_folder_missing(self):
|
||||||
|
# Test the Exception in validate() that verifies the imap_folder list
|
||||||
|
email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
|
||||||
|
email_account.imap_folder = []
|
||||||
|
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
email_account.validate()
|
||||||
|
|
||||||
class TestInboundMail(unittest.TestCase):
|
class TestInboundMail(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
"is_global": 1,
|
"is_global": 1,
|
||||||
"doctype": "Email Account",
|
"doctype": "Email Account",
|
||||||
"domain":"example.com",
|
"domain":"example.com",
|
||||||
"append_to": "ToDo",
|
|
||||||
"email_account_name": "_Test Email Account 1",
|
"email_account_name": "_Test Email Account 1",
|
||||||
"enable_outgoing": 1,
|
"enable_outgoing": 1,
|
||||||
"smtp_server": "test.example.com",
|
"smtp_server": "test.example.com",
|
||||||
|
|
@ -20,6 +19,8 @@
|
||||||
"send_notification_to": "test_unreplied@example.com",
|
"send_notification_to": "test_unreplied@example.com",
|
||||||
"pop3_server": "pop.test.example.com",
|
"pop3_server": "pop.test.example.com",
|
||||||
"no_remaining":"0",
|
"no_remaining":"0",
|
||||||
|
"append_to": "ToDo",
|
||||||
|
"imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}],
|
||||||
"track_email_status": 1
|
"track_email_status": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,213 +1,67 @@
|
||||||
{
|
{
|
||||||
"allow_copy": 1,
|
"actions": [],
|
||||||
"allow_guest_to_view": 0,
|
"allow_copy": 1,
|
||||||
"allow_import": 0,
|
"creation": "2016-04-20 15:29:39.785172",
|
||||||
"allow_rename": 0,
|
"doctype": "DocType",
|
||||||
"beta": 0,
|
"engine": "InnoDB",
|
||||||
"creation": "2016-04-20 15:29:39.785172",
|
"field_order": [
|
||||||
"custom": 0,
|
"is_completed",
|
||||||
"docstatus": 0,
|
"communication",
|
||||||
"doctype": "DocType",
|
"action",
|
||||||
"document_type": "",
|
"email_account",
|
||||||
"editable_grid": 0,
|
"uid"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"default": "0",
|
||||||
"allow_on_submit": 0,
|
"fieldname": "is_completed",
|
||||||
"bold": 0,
|
"fieldtype": "Check",
|
||||||
"collapsible": 0,
|
"label": "Is Completed",
|
||||||
"columns": 0,
|
"read_only": 1
|
||||||
"fieldname": "is_completed",
|
},
|
||||||
"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": "Is Completed",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"fieldname": "communication",
|
||||||
"allow_on_submit": 0,
|
"fieldtype": "Data",
|
||||||
"bold": 0,
|
"label": "Communication"
|
||||||
"collapsible": 0,
|
},
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "communication",
|
|
||||||
"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": "Communication",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"fieldname": "action",
|
||||||
"allow_on_submit": 0,
|
"fieldtype": "Select",
|
||||||
"bold": 0,
|
"label": "Action",
|
||||||
"collapsible": 0,
|
"options": "Read\nUnread"
|
||||||
"columns": 0,
|
},
|
||||||
"fieldname": "action",
|
|
||||||
"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": "Action",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Read\nUnread",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"fieldname": "email_account",
|
||||||
"allow_on_submit": 0,
|
"fieldtype": "Data",
|
||||||
"bold": 0,
|
"hidden": 1,
|
||||||
"collapsible": 0,
|
"label": "Email Account"
|
||||||
"columns": 0,
|
},
|
||||||
"fieldname": "email_account",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 1,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Email Account",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"fieldname": "uid",
|
||||||
"allow_on_submit": 0,
|
"fieldtype": "Data",
|
||||||
"bold": 0,
|
"hidden": 1,
|
||||||
"collapsible": 0,
|
"label": "UID"
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "uid",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 1,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "UID",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
"in_create": 1,
|
||||||
"hide_heading": 0,
|
"links": [],
|
||||||
"hide_toolbar": 0,
|
"modified": "2021-11-30 09:51:34.489932",
|
||||||
"idx": 0,
|
"modified_by": "Administrator",
|
||||||
"image_view": 0,
|
"module": "Email",
|
||||||
"in_create": 1,
|
"name": "Email Flag Queue",
|
||||||
"is_submittable": 0,
|
"owner": "Administrator",
|
||||||
"issingle": 0,
|
|
||||||
"istable": 0,
|
|
||||||
"max_attachments": 0,
|
|
||||||
"modified": "2017-09-20 15:27:12.142079",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "Email",
|
|
||||||
"name": "Email Flag Queue",
|
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"amend": 0,
|
"delete": 1,
|
||||||
"apply_user_permissions": 0,
|
"email": 1,
|
||||||
"cancel": 0,
|
"export": 1,
|
||||||
"create": 0,
|
"print": 1,
|
||||||
"delete": 1,
|
"read": 1,
|
||||||
"email": 1,
|
"report": 1,
|
||||||
"export": 1,
|
"role": "System Manager",
|
||||||
"if_owner": 0,
|
"share": 1
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "System Manager",
|
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 0,
|
"sort_field": "modified",
|
||||||
"read_only": 0,
|
"sort_order": "DESC"
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"track_changes": 0,
|
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ from frappe import _, safe_encode, task
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.email.queue import get_unsubcribed_url, get_unsubscribe_message
|
from frappe.email.queue import get_unsubcribed_url, get_unsubscribe_message
|
||||||
from frappe.email.email_body import add_attachment, get_formatted_html, get_email
|
from frappe.email.email_body import add_attachment, get_formatted_html, get_email
|
||||||
from frappe.utils import cint, split_emails, add_days, nowdate, cstr
|
from frappe.utils import cint, split_emails, add_days, nowdate, cstr, get_hook_method
|
||||||
from frappe.email.doctype.email_account.email_account import EmailAccount
|
from frappe.email.doctype.email_account.email_account import EmailAccount
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -121,9 +121,13 @@ class EmailQueue(Document):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
message = ctx.build_message(recipient.recipient)
|
message = ctx.build_message(recipient.recipient)
|
||||||
if not frappe.flags.in_test:
|
method = get_hook_method('override_email_send')
|
||||||
ctx.smtp_session.sendmail(from_addr=self.sender, to_addrs=recipient.recipient, msg=message)
|
if method:
|
||||||
ctx.add_to_sent_list(recipient)
|
method(self, self.sender, recipient.recipient, message)
|
||||||
|
else:
|
||||||
|
if not frappe.flags.in_test:
|
||||||
|
ctx.smtp_session.sendmail(from_addr=self.sender, to_addrs=recipient.recipient, msg=message)
|
||||||
|
ctx.add_to_sent_list(recipient)
|
||||||
|
|
||||||
if frappe.flags.in_test:
|
if frappe.flags.in_test:
|
||||||
frappe.flags.sent_mail = message
|
frappe.flags.sent_mail = message
|
||||||
|
|
@ -283,9 +287,14 @@ class SendMailContext:
|
||||||
if attachment.get('fcontent'):
|
if attachment.get('fcontent'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
fid = attachment.get("fid")
|
file_filters = {}
|
||||||
if fid:
|
if attachment.get('fid'):
|
||||||
_file = frappe.get_doc("File", fid)
|
file_filters['name'] = attachment.get('fid')
|
||||||
|
elif attachment.get('file_url'):
|
||||||
|
file_filters['file_url'] = attachment.get('file_url')
|
||||||
|
|
||||||
|
if file_filters:
|
||||||
|
_file = frappe.get_doc("File", file_filters)
|
||||||
fcontent = _file.get_content()
|
fcontent = _file.get_content()
|
||||||
attachment.update({
|
attachment.update({
|
||||||
'fname': _file.file_name,
|
'fname': _file.file_name,
|
||||||
|
|
@ -293,6 +302,7 @@ class SendMailContext:
|
||||||
'parent': message_obj
|
'parent': message_obj
|
||||||
})
|
})
|
||||||
attachment.pop("fid", None)
|
attachment.pop("fid", None)
|
||||||
|
attachment.pop("file_url", None)
|
||||||
add_attachment(**attachment)
|
add_attachment(**attachment)
|
||||||
|
|
||||||
elif attachment.get("print_format_attachment") == 1:
|
elif attachment.get("print_format_attachment") == 1:
|
||||||
|
|
@ -503,7 +513,7 @@ class QueueBuilder:
|
||||||
if self._attachments:
|
if self._attachments:
|
||||||
# store attachments with fid or print format details, to be attached on-demand later
|
# store attachments with fid or print format details, to be attached on-demand later
|
||||||
for att in self._attachments:
|
for att in self._attachments:
|
||||||
if att.get('fid'):
|
if att.get('fid') or att.get('file_url'):
|
||||||
attachments.append(att)
|
attachments.append(att)
|
||||||
elif att.get("print_format_attachment") == 1:
|
elif att.get("print_format_attachment") == 1:
|
||||||
if not att.get('lang', None):
|
if not att.get('lang', None):
|
||||||
|
|
|
||||||
0
frappe/email/doctype/imap_folder/__init__.py
Normal file
0
frappe/email/doctype/imap_folder/__init__.py
Normal file
53
frappe/email/doctype/imap_folder/imap_folder.json
Normal file
53
frappe/email/doctype/imap_folder/imap_folder.json
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-09-21 11:38:13.521979",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"folder_name",
|
||||||
|
"append_to",
|
||||||
|
"uidvalidity",
|
||||||
|
"uidnext"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "folder_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Folder Name",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "append_to",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Append To",
|
||||||
|
"options": "DocType"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "uidvalidity",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "UIDVALIDITY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "uidnext",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "UIDNEXT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-09-21 11:53:00.811236",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Email",
|
||||||
|
"name": "IMAP Folder",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
8
frappe/email/doctype/imap_folder/imap_folder.py
Normal file
8
frappe/email/doctype/imap_folder/imap_folder.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class IMAPFolder(Document):
|
||||||
|
pass
|
||||||
|
|
@ -4,69 +4,137 @@
|
||||||
frappe.ui.form.on('Newsletter', {
|
frappe.ui.form.on('Newsletter', {
|
||||||
refresh(frm) {
|
refresh(frm) {
|
||||||
let doc = frm.doc;
|
let doc = frm.doc;
|
||||||
if (!doc.__islocal && !cint(doc.email_sent) && !doc.__unsaved
|
let can_write = in_list(frappe.boot.user.can_write, doc.doctype);
|
||||||
&& in_list(frappe.boot.user.can_write, doc.doctype)) {
|
if (!frm.is_new() && !frm.is_dirty() && !doc.email_sent && can_write) {
|
||||||
frm.add_custom_button(__('Send Now'), function() {
|
frm.add_custom_button(__('Send a test email'), () => {
|
||||||
frappe.confirm(__("Do you really want to send this email newsletter?"), function() {
|
frm.events.send_test_email(frm);
|
||||||
frm.call('send_emails').then(() => {
|
}, __('Preview'));
|
||||||
frm.refresh();
|
|
||||||
});
|
frm.add_custom_button(__('Check broken links'), () => {
|
||||||
|
frm.dashboard.set_headline(__('Checking broken links...'));
|
||||||
|
frm.call('find_broken_links').then(r => {
|
||||||
|
frm.dashboard.set_headline('');
|
||||||
|
let links = r.message;
|
||||||
|
if (links && links.length) {
|
||||||
|
let html = '<ul>' + links.map(link => `<li>${link}</li>`).join('') + '</ul>';
|
||||||
|
frm.dashboard.set_headline(__("Following links are broken in the email content: {0}", [html]));
|
||||||
|
} else {
|
||||||
|
frm.dashboard.set_headline(__("No broken links found in the email content"));
|
||||||
|
setTimeout(() => {
|
||||||
|
frm.dashboard.set_headline('');
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, "fa fa-play", "btn-success");
|
}, __('Preview'));
|
||||||
|
|
||||||
|
frm.add_custom_button(__('Send now'), () => {
|
||||||
|
if (frm.doc.schedule_send) {
|
||||||
|
frappe.confirm(__("This newsletter was scheduled to send on a later date. Are you sure you want to send it now?"), function () {
|
||||||
|
frm.call('send_emails').then(() => frm.refresh());
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
frappe.confirm(__("Are you sure you want to send this newsletter now?"), function () {
|
||||||
|
frm.call('send_emails').then(() => frm.refresh());
|
||||||
|
});
|
||||||
|
}, __('Send'));
|
||||||
|
|
||||||
|
frm.add_custom_button(__('Schedule sending'), () => {
|
||||||
|
frm.events.schedule_send_dialog(frm);
|
||||||
|
}, __('Send'));
|
||||||
}
|
}
|
||||||
|
|
||||||
frm.events.setup_dashboard(frm);
|
frm.events.setup_dashboard(frm);
|
||||||
|
frm.events.setup_sending_status(frm);
|
||||||
|
|
||||||
if (doc.__islocal && !doc.send_from) {
|
if (frm.is_new() && !doc.sender_email) {
|
||||||
let { fullname, email } = frappe.user_info(doc.owner);
|
let { fullname, email } = frappe.user_info(doc.owner);
|
||||||
frm.set_value('send_from', `${fullname} <${email}>`);
|
frm.set_value('sender_email', email);
|
||||||
|
frm.set_value('sender_name', fullname);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frm.trigger('update_schedule_message');
|
||||||
},
|
},
|
||||||
|
|
||||||
onload_post_render(frm) {
|
schedule_send_dialog(frm) {
|
||||||
frm.trigger('setup_schedule_send');
|
let hours = frappe.utils.range(24);
|
||||||
},
|
let time_slots = hours.map(hour => {
|
||||||
|
return `${(hour + '').padStart(2, '0')}:00`;
|
||||||
setup_schedule_send(frm) {
|
});
|
||||||
let today = new Date();
|
let d = new frappe.ui.Dialog({
|
||||||
|
title: __('Schedule Newsletter'),
|
||||||
// setting datepicker options to set min date & min time
|
fields: [
|
||||||
today.setHours(today.getHours() + 1 );
|
{
|
||||||
frm.get_field('schedule_send').$input.datepicker({
|
label: __('Date'),
|
||||||
maxMinutes: 0,
|
fieldname: 'date',
|
||||||
minDate: today,
|
fieldtype: 'Date',
|
||||||
timeFormat: 'hh:00:00',
|
options: {
|
||||||
onSelect: function (fd, d, picker) {
|
minDate: new Date()
|
||||||
if (!d) return;
|
}
|
||||||
var date = d.toDateString();
|
},
|
||||||
if (date === today.toDateString()) {
|
{
|
||||||
picker.update({
|
label: __('Time'),
|
||||||
minHours: (today.getHours() + 1)
|
fieldname: 'time',
|
||||||
});
|
fieldtype: 'Select',
|
||||||
} else {
|
options: time_slots,
|
||||||
picker.update({
|
},
|
||||||
minHours: 0
|
],
|
||||||
});
|
primary_action_label: __('Schedule'),
|
||||||
}
|
primary_action({ date, time }) {
|
||||||
frm.get_field('schedule_send').$input.trigger('change');
|
frm.set_value('schedule_sending', 1);
|
||||||
|
frm.set_value('schedule_send', `${date} ${time}:00`);
|
||||||
|
d.hide();
|
||||||
|
frm.save();
|
||||||
|
},
|
||||||
|
secondary_action_label: __('Cancel Scheduling'),
|
||||||
|
secondary_action() {
|
||||||
|
frm.set_value('schedule_sending', 0);
|
||||||
|
frm.set_value('schedule_send', '');
|
||||||
|
d.hide();
|
||||||
|
frm.save();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (frm.doc.schedule_sending) {
|
||||||
|
let parts = frm.doc.schedule_send.split(' ');
|
||||||
|
if (parts.length === 2) {
|
||||||
|
let [date, time] = parts;
|
||||||
|
d.set_value('date', date);
|
||||||
|
d.set_value('time', time.slice(0, 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
send_test_email(frm) {
|
||||||
const $tp = frm.get_field('schedule_send').datepicker.timepicker;
|
let d = new frappe.ui.Dialog({
|
||||||
$tp.$minutes.parent().css('display', 'none');
|
title: __('Send Test Email'),
|
||||||
$tp.$minutesText.css('display', 'none');
|
fields: [
|
||||||
$tp.$minutesText.prev().css('display', 'none');
|
{
|
||||||
$tp.$seconds.parent().css('display', 'none');
|
label: __('Email'),
|
||||||
|
fieldname: 'email',
|
||||||
|
fieldtype: 'Data',
|
||||||
|
options: 'Email',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
primary_action_label: __('Send'),
|
||||||
|
primary_action({ email }) {
|
||||||
|
d.get_primary_btn().text(__('Sending...')).prop('disabled', true);
|
||||||
|
frm.call('send_test_email', { email })
|
||||||
|
.then(() => {
|
||||||
|
d.get_primary_btn().text(__('Send again')).prop('disabled', false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
d.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
setup_dashboard(frm) {
|
setup_dashboard(frm) {
|
||||||
if(!frm.doc.__islocal && cint(frm.doc.email_sent)
|
if (!frm.doc.__islocal && cint(frm.doc.email_sent)
|
||||||
&& frm.doc.__onload && frm.doc.__onload.status_count) {
|
&& frm.doc.__onload && frm.doc.__onload.status_count) {
|
||||||
var stat = frm.doc.__onload.status_count;
|
var stat = frm.doc.__onload.status_count;
|
||||||
var total = frm.doc.scheduled_to_send;
|
var total = frm.doc.scheduled_to_send;
|
||||||
if(total) {
|
if (total) {
|
||||||
$.each(stat, function(k, v) {
|
$.each(stat, function (k, v) {
|
||||||
stat[k] = flt(v * 100 / total, 2) + '%';
|
stat[k] = flt(v * 100 / total, 2) + '%';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -94,5 +162,58 @@ frappe.ui.form.on('Newsletter', {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setup_sending_status(frm) {
|
||||||
|
frm.call('get_sending_status').then(r => {
|
||||||
|
if (r.message) {
|
||||||
|
frm.events.update_sending_progress(frm, r.message.sent, r.message.total);
|
||||||
|
}
|
||||||
|
if (r.message.sent >= r.message.total) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (frm.sending_status) return;
|
||||||
|
|
||||||
|
frm.sending_status = setInterval(() => {
|
||||||
|
if (frm.doc.email_sent && frm.$wrapper.is(':visible')) {
|
||||||
|
frm.call('get_sending_status').then(r => {
|
||||||
|
if (r.message) {
|
||||||
|
let { sent, total } = r.message;
|
||||||
|
frm.events.update_sending_progress(frm, sent, total);
|
||||||
|
|
||||||
|
if (sent >= total) {
|
||||||
|
clearInterval(frm.sending_status);
|
||||||
|
frm.sending_status = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
update_sending_progress(frm, sent, total) {
|
||||||
|
if (sent >= total) {
|
||||||
|
frm.dashboard.hide_progress();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
frm.dashboard.show_progress(__('Sending emails'), sent * 100 / total, __("{0} of {1} sent", [sent, total]));
|
||||||
|
},
|
||||||
|
|
||||||
|
on_hide(frm) {
|
||||||
|
if (frm.sending_status) {
|
||||||
|
clearInterval(frm.sending_status);
|
||||||
|
frm.sending_status = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
update_schedule_message(frm) {
|
||||||
|
if (!frm.doc.email_sent && frm.doc.schedule_send) {
|
||||||
|
let datetime = frappe.datetime.global_date_format(frm.doc.schedule_send);
|
||||||
|
frm.dashboard.set_headline_alert(__('This newsletter is scheduled to be sent on {0}', [datetime.bold()]));
|
||||||
|
} else {
|
||||||
|
frm.dashboard.clear_headline();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,48 +7,59 @@
|
||||||
"document_type": "Other",
|
"document_type": "Other",
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"status_section",
|
||||||
|
"email_sent_at",
|
||||||
|
"column_break_3",
|
||||||
|
"total_recipients",
|
||||||
|
"column_break_12",
|
||||||
|
"email_sent",
|
||||||
|
"from_section",
|
||||||
|
"sender_name",
|
||||||
|
"column_break_5",
|
||||||
|
"sender_email",
|
||||||
|
"column_break_7",
|
||||||
"send_from",
|
"send_from",
|
||||||
"schedule_sending",
|
|
||||||
"schedule_send",
|
|
||||||
"recipients",
|
"recipients",
|
||||||
"email_group",
|
"email_group",
|
||||||
"email_sent",
|
"subject_section",
|
||||||
"newsletter_content",
|
|
||||||
"subject",
|
"subject",
|
||||||
|
"newsletter_content",
|
||||||
"content_type",
|
"content_type",
|
||||||
"message",
|
"message",
|
||||||
"message_md",
|
"message_md",
|
||||||
"message_html",
|
"message_html",
|
||||||
"section_break_13",
|
"attachments",
|
||||||
"send_unsubscribe_link",
|
"send_unsubscribe_link",
|
||||||
"send_attachments",
|
|
||||||
"column_break_9",
|
|
||||||
"published",
|
|
||||||
"send_webview_link",
|
"send_webview_link",
|
||||||
"route",
|
"schedule_settings_section",
|
||||||
"test_the_newsletter",
|
"scheduled_to_send",
|
||||||
"test_email_id",
|
"schedule_sending",
|
||||||
"test_send",
|
"schedule_send",
|
||||||
"scheduled_to_send"
|
"publish_as_a_web_page_section",
|
||||||
|
"published",
|
||||||
|
"route"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldname": "email_group",
|
"fieldname": "email_group",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Email Group",
|
"label": "Audience",
|
||||||
"options": "Newsletter Email Group"
|
"options": "Newsletter Email Group",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "send_from",
|
"fieldname": "send_from",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"ignore_xss_filter": 1,
|
"ignore_xss_filter": 1,
|
||||||
"label": "Sender"
|
"label": "Sender",
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "email_sent",
|
"fieldname": "email_sent",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
"label": "Email Sent",
|
"label": "Email Sent",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
|
@ -87,32 +98,12 @@
|
||||||
"label": "Published"
|
"label": "Published"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "published",
|
||||||
"fieldname": "route",
|
"fieldname": "route",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
|
||||||
"label": "Route",
|
"label": "Route",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "test_the_newsletter",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Testing"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "A Lead with this Email Address should exist",
|
|
||||||
"fieldname": "test_email_id",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Test Email Address",
|
|
||||||
"options": "Email"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval: doc.test_email_id",
|
|
||||||
"fieldname": "test_send",
|
|
||||||
"fieldtype": "Button",
|
|
||||||
"label": "Test",
|
|
||||||
"options": "test_send"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "scheduled_to_send",
|
"fieldname": "scheduled_to_send",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
|
|
@ -122,21 +113,16 @@
|
||||||
{
|
{
|
||||||
"fieldname": "recipients",
|
"fieldname": "recipients",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Recipients"
|
"label": "To"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: doc.schedule_sending",
|
"depends_on": "eval: doc.schedule_sending",
|
||||||
"fieldname": "schedule_send",
|
"fieldname": "schedule_send",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
"label": "Schedule Send",
|
"label": "Send Email At",
|
||||||
|
"read_only": 1,
|
||||||
"read_only_depends_on": "eval: doc.email_sent"
|
"read_only_depends_on": "eval: doc.email_sent"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "send_attachments",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Send Attachments"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "content_type",
|
"fieldname": "content_type",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
|
|
@ -161,23 +147,87 @@
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "schedule_sending",
|
"fieldname": "schedule_sending",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Schedule Sending",
|
"label": "Schedule sending at a later time",
|
||||||
"read_only_depends_on": "eval: doc.email_sent"
|
"read_only_depends_on": "eval: doc.email_sent"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break_9",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "published",
|
|
||||||
"fieldname": "send_webview_link",
|
"fieldname": "send_webview_link",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Send Web View Link"
|
"label": "Send Web View Link"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_13",
|
"fieldname": "from_section",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break",
|
||||||
|
"label": "From"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "sender_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Sender Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "sender_email",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Sender Email",
|
||||||
|
"options": "Email",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_5",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_7",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "subject_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Subject"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "publish_as_a_web_page_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Publish as a web page"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "schedule_sending",
|
||||||
|
"fieldname": "schedule_settings_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Scheduled Sending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "attachments",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Attachments",
|
||||||
|
"options": "Newsletter Attachment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "email_sent_at",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"label": "Email Sent At",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "total_recipients",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Total Recipients",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "email_sent",
|
||||||
|
"fieldname": "status_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Status"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_12",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_3",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 1,
|
"has_web_view": 1,
|
||||||
|
|
@ -187,7 +237,7 @@
|
||||||
"is_published_field": "published",
|
"is_published_field": "published",
|
||||||
"links": [],
|
"links": [],
|
||||||
"max_attachments": 3,
|
"max_attachments": 3,
|
||||||
"modified": "2021-02-22 14:33:56.095380",
|
"modified": "2021-12-06 20:09:37.963141",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Email",
|
"module": "Email",
|
||||||
"name": "Newsletter",
|
"name": "Newsletter",
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,11 @@ from .exceptions import NewsletterAlreadySentError, NoRecipientFoundError, Newsl
|
||||||
|
|
||||||
|
|
||||||
class Newsletter(WebsiteGenerator):
|
class Newsletter(WebsiteGenerator):
|
||||||
def onload(self):
|
|
||||||
self.setup_newsletter_status()
|
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.route = f"newsletters/{self.name}"
|
self.route = f"newsletters/{self.name}"
|
||||||
self.validate_sender_address()
|
self.validate_sender_address()
|
||||||
self.validate_recipient_address()
|
self.validate_recipient_address()
|
||||||
|
self.validate_publishing()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def newsletter_recipients(self) -> List[str]:
|
def newsletter_recipients(self) -> List[str]:
|
||||||
|
|
@ -30,29 +28,55 @@ class Newsletter(WebsiteGenerator):
|
||||||
return self._recipients
|
return self._recipients
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def test_send(self):
|
def get_sending_status(self):
|
||||||
test_emails = frappe.utils.split_emails(self.test_email_id)
|
count_by_status = frappe.get_all("Email Queue",
|
||||||
self.queue_all(test_emails=test_emails)
|
filters={"reference_doctype": self.doctype, "reference_name": self.name},
|
||||||
frappe.msgprint(_("Test email sent to {0}").format(self.test_email_id))
|
fields=["status", "count(name) as count"],
|
||||||
|
group_by="status",
|
||||||
|
order_by="status"
|
||||||
|
)
|
||||||
|
sent = 0
|
||||||
|
total = 0
|
||||||
|
for row in count_by_status:
|
||||||
|
if row.status == "Sent":
|
||||||
|
sent = row.count
|
||||||
|
total += row.count
|
||||||
|
|
||||||
|
return {'sent': sent, 'total': total}
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def send_test_email(self, email):
|
||||||
|
test_emails = frappe.utils.validate_email_address(email, throw=True)
|
||||||
|
self.send_newsletter(emails=test_emails)
|
||||||
|
frappe.msgprint(_("Test email sent to {0}").format(email), alert=True)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def find_broken_links(self):
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import requests
|
||||||
|
|
||||||
|
html = self.get_message()
|
||||||
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
links = soup.find_all("a")
|
||||||
|
images = soup.find_all("img")
|
||||||
|
broken_links = []
|
||||||
|
for el in links + images:
|
||||||
|
url = el.attrs.get("href") or el.attrs.get("src")
|
||||||
|
try:
|
||||||
|
response = requests.head(url, verify=False, timeout=5)
|
||||||
|
if response.status_code >= 400:
|
||||||
|
broken_links.append(url)
|
||||||
|
except:
|
||||||
|
broken_links.append(url)
|
||||||
|
return broken_links
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def send_emails(self):
|
def send_emails(self):
|
||||||
"""send emails to leads and customers"""
|
"""queue sending emails to recipients"""
|
||||||
|
self.schedule_sending = False
|
||||||
|
self.schedule_send = None
|
||||||
self.queue_all()
|
self.queue_all()
|
||||||
frappe.msgprint(_("Email queued to {0} recipients").format(len(self.newsletter_recipients)))
|
frappe.msgprint(_("Email queued to {0} recipients").format(self.total_recipients))
|
||||||
|
|
||||||
def setup_newsletter_status(self):
|
|
||||||
"""Setup analytical status for current Newsletter. Can be accessible from desk.
|
|
||||||
"""
|
|
||||||
if self.email_sent:
|
|
||||||
status_count = frappe.get_all("Email Queue",
|
|
||||||
filters={"reference_doctype": self.doctype, "reference_name": self.name},
|
|
||||||
fields=["status", "count(name)"],
|
|
||||||
group_by="status",
|
|
||||||
order_by="status",
|
|
||||||
as_list=True,
|
|
||||||
)
|
|
||||||
self.get("__onload").status_count = dict(status_count)
|
|
||||||
|
|
||||||
def validate_send(self):
|
def validate_send(self):
|
||||||
"""Validate if Newsletter can be sent.
|
"""Validate if Newsletter can be sent.
|
||||||
|
|
@ -75,8 +99,9 @@ class Newsletter(WebsiteGenerator):
|
||||||
def validate_sender_address(self):
|
def validate_sender_address(self):
|
||||||
"""Validate self.send_from is a valid email address or not.
|
"""Validate self.send_from is a valid email address or not.
|
||||||
"""
|
"""
|
||||||
if self.send_from:
|
if self.sender_email:
|
||||||
frappe.utils.validate_email_address(self.send_from, throw=True)
|
frappe.utils.validate_email_address(self.sender_email, throw=True)
|
||||||
|
self.send_from = f"{self.sender_name} <{self.sender_email}>" if self.sender_name else self.sender_email
|
||||||
|
|
||||||
def validate_recipient_address(self):
|
def validate_recipient_address(self):
|
||||||
"""Validate if self.newsletter_recipients are all valid email addresses or not.
|
"""Validate if self.newsletter_recipients are all valid email addresses or not.
|
||||||
|
|
@ -84,6 +109,10 @@ class Newsletter(WebsiteGenerator):
|
||||||
for recipient in self.newsletter_recipients:
|
for recipient in self.newsletter_recipients:
|
||||||
frappe.utils.validate_email_address(recipient, throw=True)
|
frappe.utils.validate_email_address(recipient, throw=True)
|
||||||
|
|
||||||
|
def validate_publishing(self):
|
||||||
|
if self.send_webview_link and not self.published:
|
||||||
|
frappe.throw(_("Newsletter must be published to send webview link in email"))
|
||||||
|
|
||||||
def get_linked_email_queue(self) -> List[str]:
|
def get_linked_email_queue(self) -> List[str]:
|
||||||
"""Get list of email queue linked to this newsletter.
|
"""Get list of email queue linked to this newsletter.
|
||||||
"""
|
"""
|
||||||
|
|
@ -116,45 +145,24 @@ class Newsletter(WebsiteGenerator):
|
||||||
x for x in self.newsletter_recipients if x not in self.get_success_recipients()
|
x for x in self.newsletter_recipients if x not in self.get_success_recipients()
|
||||||
]
|
]
|
||||||
|
|
||||||
def queue_all(self, test_emails: List[str] = None):
|
def queue_all(self):
|
||||||
"""Queue Newsletter to all the recipients generated from the `Email Group`
|
"""Queue Newsletter to all the recipients generated from the `Email Group` table
|
||||||
table
|
|
||||||
|
|
||||||
Args:
|
|
||||||
test_email (List[str], optional): Send test Newsletter to the passed set of emails.
|
|
||||||
Defaults to None.
|
|
||||||
"""
|
"""
|
||||||
if test_emails:
|
self.validate()
|
||||||
for test_email in test_emails:
|
self.validate_send()
|
||||||
frappe.utils.validate_email_address(test_email, throw=True)
|
|
||||||
else:
|
|
||||||
self.validate()
|
|
||||||
self.validate_send()
|
|
||||||
|
|
||||||
newsletter_recipients = test_emails or self.get_pending_recipients()
|
recipients = self.get_pending_recipients()
|
||||||
self.send_newsletter(emails=newsletter_recipients)
|
self.send_newsletter(emails=recipients)
|
||||||
|
|
||||||
if not test_emails:
|
self.email_sent = True
|
||||||
self.email_sent = True
|
self.email_sent_at = frappe.utils.now()
|
||||||
self.schedule_send = frappe.utils.now_datetime()
|
self.total_recipients = len(recipients)
|
||||||
self.scheduled_to_send = len(newsletter_recipients)
|
self.save()
|
||||||
self.save()
|
|
||||||
|
|
||||||
def get_newsletter_attachments(self) -> List[Dict[str, str]]:
|
def get_newsletter_attachments(self) -> List[Dict[str, str]]:
|
||||||
"""Get list of attachments on current Newsletter
|
"""Get list of attachments on current Newsletter
|
||||||
"""
|
"""
|
||||||
attachments = []
|
return [{"file_url": row.attachment} for row in self.attachments]
|
||||||
|
|
||||||
if self.send_attachments:
|
|
||||||
files = frappe.get_all(
|
|
||||||
"File",
|
|
||||||
filters={"attached_to_doctype": "Newsletter", "attached_to_name": self.name},
|
|
||||||
order_by="creation desc",
|
|
||||||
pluck="name",
|
|
||||||
)
|
|
||||||
attachments.extend({"fid": file} for file in files)
|
|
||||||
|
|
||||||
return attachments
|
|
||||||
|
|
||||||
def send_newsletter(self, emails: List[str]):
|
def send_newsletter(self, emails: List[str]):
|
||||||
"""Trigger email generation for `emails` and add it in Email Queue.
|
"""Trigger email generation for `emails` and add it in Email Queue.
|
||||||
|
|
@ -186,12 +194,13 @@ class Newsletter(WebsiteGenerator):
|
||||||
frappe.db.auto_commit_on_many_writes = is_auto_commit_set
|
frappe.db.auto_commit_on_many_writes = is_auto_commit_set
|
||||||
|
|
||||||
def get_message(self) -> str:
|
def get_message(self) -> str:
|
||||||
if self.content_type == "HTML":
|
message = self.message
|
||||||
return frappe.render_template(self.message_html, {"doc": self.as_dict()})
|
|
||||||
if self.content_type == "Markdown":
|
if self.content_type == "Markdown":
|
||||||
return frappe.utils.markdown(self.message_md)
|
message = frappe.utils.md_to_html(self.message_md)
|
||||||
# fallback to Rich Text
|
if self.content_type == "HTML":
|
||||||
return self.message
|
message = self.message_html
|
||||||
|
|
||||||
|
return frappe.render_template(message, {"doc": self.as_dict()})
|
||||||
|
|
||||||
def get_recipients(self) -> List[str]:
|
def get_recipients(self) -> List[str]:
|
||||||
"""Get recipients from Email Group"""
|
"""Get recipients from Email Group"""
|
||||||
|
|
@ -223,21 +232,6 @@ class Newsletter(WebsiteGenerator):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_context(self, context):
|
|
||||||
newsletters = get_newsletter_list("Newsletter", None, None, 0)
|
|
||||||
if newsletters:
|
|
||||||
newsletter_list = [d.name for d in newsletters]
|
|
||||||
if self.name not in newsletter_list:
|
|
||||||
frappe.redirect_to_message(
|
|
||||||
_("Permission Error"), _("You are not permitted to view the newsletter.")
|
|
||||||
)
|
|
||||||
frappe.local.flags.redirect_location = frappe.local.response.location
|
|
||||||
raise frappe.Redirect
|
|
||||||
else:
|
|
||||||
context.attachments = self.get_attachments()
|
|
||||||
context.no_cache = 1
|
|
||||||
context.show_sidebar = True
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def confirmed_unsubscribe(email, group):
|
def confirmed_unsubscribe(email, group):
|
||||||
|
|
@ -320,35 +314,14 @@ def confirm_subscription(email, email_group=_("Website")):
|
||||||
|
|
||||||
def get_list_context(context=None):
|
def get_list_context(context=None):
|
||||||
context.update({
|
context.update({
|
||||||
"show_sidebar": True,
|
|
||||||
"show_search": True,
|
"show_search": True,
|
||||||
'no_breadcrumbs': True,
|
"no_breadcrumbs": True,
|
||||||
"title": _("Newsletter"),
|
"title": _("Newsletters"),
|
||||||
"get_list": get_newsletter_list,
|
"filters": {"published": 1},
|
||||||
"row_template": "email/doctype/newsletter/templates/newsletter_row.html",
|
"row_template": "email/doctype/newsletter/templates/newsletter_row.html",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def get_newsletter_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"):
|
|
||||||
email_group_list = frappe.db.sql('''SELECT eg.name
|
|
||||||
FROM `tabEmail Group` eg, `tabEmail Group Member` egm
|
|
||||||
WHERE egm.unsubscribed=0
|
|
||||||
AND eg.name=egm.email_group
|
|
||||||
AND egm.email = %s''', frappe.session.user)
|
|
||||||
email_group_list = [d[0] for d in email_group_list]
|
|
||||||
|
|
||||||
if email_group_list:
|
|
||||||
return frappe.db.sql('''SELECT n.name, n.subject, n.message, n.modified
|
|
||||||
FROM `tabNewsletter` n, `tabNewsletter Email Group` neg
|
|
||||||
WHERE n.name = neg.parent
|
|
||||||
AND n.email_sent=1
|
|
||||||
AND n.published=1
|
|
||||||
AND neg.email_group in ({0})
|
|
||||||
ORDER BY n.modified DESC LIMIT {1} OFFSET {2}
|
|
||||||
'''.format(','.join(['%s'] * len(email_group_list)),
|
|
||||||
limit_page_length, limit_start), email_group_list, as_dict=1)
|
|
||||||
|
|
||||||
|
|
||||||
def send_scheduled_email():
|
def send_scheduled_email():
|
||||||
"""Send scheduled newsletter to the recipients."""
|
"""Send scheduled newsletter to the recipients."""
|
||||||
scheduled_newsletter = frappe.get_all(
|
scheduled_newsletter = frappe.get_all(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends "templates/web.html" %}
|
{% extends "templates/web.html" %}
|
||||||
|
|
||||||
{% block title %} {{ _("Newsletter") }} {% endblock %}
|
{% block title %} {{ doc.subject }} {% endblock %}
|
||||||
|
|
||||||
{% block page_content %}
|
{% block page_content %}
|
||||||
<style>
|
<style>
|
||||||
|
|
@ -36,11 +36,11 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div itemprop="articleBody" class="longform blog-text">
|
<div itemprop="articleBody" class="longform blog-text">
|
||||||
{{ doc.message }}
|
{{ doc.get_message() }}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
{% if attachments %}
|
{% if doc.attachments %}
|
||||||
<div>
|
<div>
|
||||||
<div class="row text-muted">
|
<div class="row text-muted">
|
||||||
<div class="col-sm-12 h6 text-uppercase">
|
<div class="col-sm-12 h6 text-uppercase">
|
||||||
|
|
@ -49,10 +49,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
{% for attachment in attachments %}
|
{% for attachment in doc.attachments %}
|
||||||
<p class="small">
|
<p class="small">
|
||||||
<a href="{{ attachment.file_url }}" target="blank">
|
<a href="{{ attachment.attachment }}" target="_blank">
|
||||||
{{ attachment.file_name }}
|
{{ attachment.attachment }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ from frappe.email.doctype.newsletter.exceptions import (
|
||||||
from frappe.email.doctype.newsletter.newsletter import (
|
from frappe.email.doctype.newsletter.newsletter import (
|
||||||
Newsletter,
|
Newsletter,
|
||||||
confirmed_unsubscribe,
|
confirmed_unsubscribe,
|
||||||
get_newsletter_list,
|
|
||||||
send_scheduled_email
|
send_scheduled_email
|
||||||
)
|
)
|
||||||
from frappe.email.queue import flush
|
from frappe.email.queue import flush
|
||||||
|
|
@ -101,7 +100,8 @@ class TestNewsletterMixin:
|
||||||
doctype = "Newsletter"
|
doctype = "Newsletter"
|
||||||
newsletter_content = {
|
newsletter_content = {
|
||||||
"subject": "_Test Newsletter",
|
"subject": "_Test Newsletter",
|
||||||
"send_from": "Test Sender <test_sender@example.com>",
|
"sender_name": "Test Sender",
|
||||||
|
"sender_email": "test_sender@example.com",
|
||||||
"content_type": "Rich Text",
|
"content_type": "Rich Text",
|
||||||
"message": "Testing my news.",
|
"message": "Testing my news.",
|
||||||
}
|
}
|
||||||
|
|
@ -157,21 +157,6 @@ class TestNewsletter(TestNewsletterMixin, unittest.TestCase):
|
||||||
if email != to_unsubscribe:
|
if email != to_unsubscribe:
|
||||||
self.assertTrue(email in recipients)
|
self.assertTrue(email in recipients)
|
||||||
|
|
||||||
def test_portal(self):
|
|
||||||
self.send_newsletter(published=1)
|
|
||||||
frappe.set_user("test1@example.com")
|
|
||||||
newsletter_list = get_newsletter_list("Newsletter", None, None, 0)
|
|
||||||
self.assertEqual(len(newsletter_list), 1)
|
|
||||||
|
|
||||||
def test_newsletter_context(self):
|
|
||||||
context = frappe._dict()
|
|
||||||
newsletter_name = self.send_newsletter(published=1)
|
|
||||||
frappe.set_user("test2@example.com")
|
|
||||||
doc = frappe.get_doc("Newsletter", newsletter_name)
|
|
||||||
doc.get_context(context)
|
|
||||||
self.assertEqual(context.no_cache, 1)
|
|
||||||
self.assertTrue("attachments" not in list(context))
|
|
||||||
|
|
||||||
def test_schedule_send(self):
|
def test_schedule_send(self):
|
||||||
self.send_newsletter(schedule_send=add_days(getdate(), -1))
|
self.send_newsletter(schedule_send=add_days(getdate(), -1))
|
||||||
|
|
||||||
|
|
@ -181,26 +166,32 @@ class TestNewsletter(TestNewsletterMixin, unittest.TestCase):
|
||||||
for email in emails:
|
for email in emails:
|
||||||
self.assertTrue(email in recipients)
|
self.assertTrue(email in recipients)
|
||||||
|
|
||||||
def test_newsletter_test_send(self):
|
def test_newsletter_send_test_email(self):
|
||||||
"""Test "Test Send" functionality of Newsletter
|
"""Test "Send Test Email" functionality of Newsletter
|
||||||
"""
|
"""
|
||||||
newsletter = self.get_newsletter()
|
newsletter = self.get_newsletter()
|
||||||
newsletter.test_email_id = choice(emails)
|
test_email = choice(emails)
|
||||||
newsletter.test_send()
|
newsletter.send_test_email(test_email)
|
||||||
|
|
||||||
self.assertFalse(newsletter.email_sent)
|
self.assertFalse(newsletter.email_sent)
|
||||||
newsletter.save = MagicMock()
|
newsletter.save = MagicMock()
|
||||||
self.assertFalse(newsletter.save.called)
|
self.assertFalse(newsletter.save.called)
|
||||||
|
# check if the test email is in the queue
|
||||||
|
email_queue = frappe.db.get_all('Email Queue', filters=[
|
||||||
|
['reference_doctype', '=', 'Newsletter'],
|
||||||
|
['reference_name', '=', newsletter.name],
|
||||||
|
['Email Queue Recipient', 'recipient', '=', test_email]
|
||||||
|
])
|
||||||
|
self.assertTrue(email_queue)
|
||||||
|
|
||||||
def test_newsletter_status(self):
|
def test_newsletter_status(self):
|
||||||
"""Test for Newsletter's stats on onload event
|
"""Test for Newsletter's stats on onload event
|
||||||
"""
|
"""
|
||||||
newsletter = self.get_newsletter()
|
newsletter = self.get_newsletter()
|
||||||
newsletter.email_sent = True
|
newsletter.email_sent = True
|
||||||
# had to use run_onload as calling .onload directly bought weird errors
|
result = newsletter.get_sending_status()
|
||||||
# like TestNewsletter has no attribute "_TestNewsletter__onload"
|
self.assertTrue('total' in result)
|
||||||
run_onload(newsletter)
|
self.assertTrue('sent' in result)
|
||||||
self.assertIsInstance(newsletter.get("__onload").status_count, dict)
|
|
||||||
|
|
||||||
def test_already_sent_newsletter(self):
|
def test_already_sent_newsletter(self):
|
||||||
newsletter = self.get_newsletter()
|
newsletter = self.get_newsletter()
|
||||||
|
|
@ -218,22 +209,6 @@ class TestNewsletter(TestNewsletterMixin, unittest.TestCase):
|
||||||
with self.assertRaises(NoRecipientFoundError):
|
with self.assertRaises(NoRecipientFoundError):
|
||||||
newsletter.send_emails()
|
newsletter.send_emails()
|
||||||
|
|
||||||
def test_send_newsletter_with_attachments(self):
|
|
||||||
newsletter = self.get_newsletter()
|
|
||||||
newsletter.reload()
|
|
||||||
file_attachment = frappe.get_doc({
|
|
||||||
"doctype": "File",
|
|
||||||
"file_name": "test1.txt",
|
|
||||||
"attached_to_doctype": newsletter.doctype,
|
|
||||||
"attached_to_name": newsletter.name,
|
|
||||||
"content": frappe.mock("paragraph")
|
|
||||||
})
|
|
||||||
file_attachment.save()
|
|
||||||
newsletter.send_attachments = True
|
|
||||||
newsletter_attachments = newsletter.get_newsletter_attachments()
|
|
||||||
self.assertEqual(len(newsletter_attachments), 1)
|
|
||||||
self.assertEqual(newsletter_attachments[0]["fid"], file_attachment.name)
|
|
||||||
|
|
||||||
def test_send_scheduled_email_error_handling(self):
|
def test_send_scheduled_email_error_handling(self):
|
||||||
newsletter = self.get_newsletter(schedule_send=add_days(getdate(), -1))
|
newsletter = self.get_newsletter(schedule_send=add_days(getdate(), -1))
|
||||||
job_path = "frappe.email.doctype.newsletter.newsletter.Newsletter.queue_all"
|
job_path = "frappe.email.doctype.newsletter.newsletter.Newsletter.queue_all"
|
||||||
|
|
|
||||||
0
frappe/email/doctype/newsletter_attachment/__init__.py
Normal file
0
frappe/email/doctype/newsletter_attachment/__init__.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2021-12-06 16:37:40.652468",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"attachment"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "attachment",
|
||||||
|
"fieldtype": "Attach",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Attachment",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-12-06 16:37:47.481057",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Email",
|
||||||
|
"name": "Newsletter Attachment",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class NewsletterAttachment(Document):
|
||||||
|
pass
|
||||||
|
|
@ -1,106 +1,42 @@
|
||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_guest_to_view": 0,
|
"creation": "2017-02-26 16:20:52.654136",
|
||||||
"allow_import": 0,
|
"doctype": "DocType",
|
||||||
"allow_rename": 0,
|
"editable_grid": 1,
|
||||||
"beta": 0,
|
"engine": "InnoDB",
|
||||||
"creation": "2017-02-26 16:20:52.654136",
|
"field_order": [
|
||||||
"custom": 0,
|
"email_group",
|
||||||
"docstatus": 0,
|
"total_subscribers"
|
||||||
"doctype": "DocType",
|
],
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"columns": 7,
|
||||||
"allow_on_submit": 0,
|
"fieldname": "email_group",
|
||||||
"bold": 0,
|
"fieldtype": "Link",
|
||||||
"collapsible": 0,
|
"in_list_view": 1,
|
||||||
"columns": 0,
|
"label": "Email Group",
|
||||||
"fieldname": "email_group",
|
"options": "Email Group",
|
||||||
"fieldtype": "Link",
|
"reqd": 1
|
||||||
"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": "Email Group",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Email Group",
|
|
||||||
"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,
|
"columns": 3,
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fetch_from": "email_group.total_subscribers",
|
"fetch_from": "email_group.total_subscribers",
|
||||||
"fieldname": "total_subscribers",
|
"fieldname": "total_subscribers",
|
||||||
"fieldtype": "Read Only",
|
"fieldtype": "Read Only",
|
||||||
"hidden": 0,
|
"in_list_view": 1,
|
||||||
"ignore_user_permissions": 0,
|
"label": "Total Subscribers"
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Total Subscribers",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "",
|
|
||||||
"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,
|
"istable": 1,
|
||||||
"hide_heading": 0,
|
"links": [],
|
||||||
"hide_toolbar": 0,
|
"modified": "2021-12-06 20:12:08.420240",
|
||||||
"idx": 0,
|
"modified_by": "Administrator",
|
||||||
"image_view": 0,
|
"module": "Email",
|
||||||
"in_create": 0,
|
"name": "Newsletter Email Group",
|
||||||
"is_submittable": 0,
|
"owner": "Administrator",
|
||||||
"issingle": 0,
|
"permissions": [],
|
||||||
"istable": 1,
|
"quick_entry": 1,
|
||||||
"max_attachments": 0,
|
"sort_field": "modified",
|
||||||
"modified": "2018-05-16 22:42:55.437367",
|
"sort_order": "DESC",
|
||||||
"modified_by": "Administrator",
|
"track_changes": 1
|
||||||
"module": "Email",
|
|
||||||
"name": "Newsletter Email Group",
|
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [],
|
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
@ -111,7 +111,17 @@ class EmailServer:
|
||||||
frappe.msgprint(_('Invalid User Name or Support Password. Please rectify and try again.'))
|
frappe.msgprint(_('Invalid User Name or Support Password. Please rectify and try again.'))
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def get_messages(self):
|
def select_imap_folder(self, folder):
|
||||||
|
self.imap.select(folder)
|
||||||
|
|
||||||
|
def logout(self):
|
||||||
|
if cint(self.settings.use_imap):
|
||||||
|
self.imap.logout()
|
||||||
|
else:
|
||||||
|
self.pop.quit()
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_messages(self, folder="INBOX"):
|
||||||
"""Returns new email messages in a list."""
|
"""Returns new email messages in a list."""
|
||||||
if not (self.check_mails() or self.connect()):
|
if not (self.check_mails() or self.connect()):
|
||||||
return []
|
return []
|
||||||
|
|
@ -126,7 +136,8 @@ class EmailServer:
|
||||||
self.latest_messages = []
|
self.latest_messages = []
|
||||||
self.seen_status = {}
|
self.seen_status = {}
|
||||||
self.uid_reindexed = False
|
self.uid_reindexed = False
|
||||||
uid_list = email_list = self.get_new_mails()
|
|
||||||
|
uid_list = email_list = self.get_new_mails(folder)
|
||||||
|
|
||||||
if not email_list:
|
if not email_list:
|
||||||
return
|
return
|
||||||
|
|
@ -160,13 +171,6 @@ class EmailServer:
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
finally:
|
|
||||||
# no matter the exception, pop should quit if connected
|
|
||||||
if cint(self.settings.use_imap):
|
|
||||||
self.imap.logout()
|
|
||||||
else:
|
|
||||||
self.pop.quit()
|
|
||||||
|
|
||||||
out = { "latest_messages": self.latest_messages }
|
out = { "latest_messages": self.latest_messages }
|
||||||
if self.settings.use_imap:
|
if self.settings.use_imap:
|
||||||
out.update({
|
out.update({
|
||||||
|
|
@ -177,15 +181,15 @@ class EmailServer:
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def get_new_mails(self):
|
def get_new_mails(self, folder):
|
||||||
"""Return list of new mails"""
|
"""Return list of new mails"""
|
||||||
if cint(self.settings.use_imap):
|
if cint(self.settings.use_imap):
|
||||||
email_list = []
|
email_list = []
|
||||||
self.check_imap_uidvalidity()
|
self.check_imap_uidvalidity(folder)
|
||||||
|
|
||||||
readonly = False if self.settings.email_sync_rule == "UNSEEN" else True
|
readonly = False if self.settings.email_sync_rule == "UNSEEN" else True
|
||||||
|
|
||||||
self.imap.select("Inbox", readonly=readonly)
|
self.imap.select(folder, readonly=readonly)
|
||||||
response, message = self.imap.uid('search', None, self.settings.email_sync_rule)
|
response, message = self.imap.uid('search', None, self.settings.email_sync_rule)
|
||||||
if message[0]:
|
if message[0]:
|
||||||
email_list = message[0].split()
|
email_list = message[0].split()
|
||||||
|
|
@ -194,11 +198,11 @@ class EmailServer:
|
||||||
|
|
||||||
return email_list
|
return email_list
|
||||||
|
|
||||||
def check_imap_uidvalidity(self):
|
def check_imap_uidvalidity(self, folder):
|
||||||
# compare the UIDVALIDITY of email account and imap server
|
# compare the UIDVALIDITY of email account and imap server
|
||||||
uid_validity = self.settings.uid_validity
|
uid_validity = self.settings.uid_validity
|
||||||
|
|
||||||
response, message = self.imap.status("Inbox", "(UIDVALIDITY UIDNEXT)")
|
response, message = self.imap.status(folder, "(UIDVALIDITY UIDNEXT)")
|
||||||
current_uid_validity = self.parse_imap_response("UIDVALIDITY", message[0]) or 0
|
current_uid_validity = self.parse_imap_response("UIDVALIDITY", message[0]) or 0
|
||||||
|
|
||||||
uidnext = int(self.parse_imap_response("UIDNEXT", message[0]) or "1")
|
uidnext = int(self.parse_imap_response("UIDNEXT", message[0]) or "1")
|
||||||
|
|
@ -206,14 +210,26 @@ class EmailServer:
|
||||||
|
|
||||||
if not uid_validity or uid_validity != current_uid_validity:
|
if not uid_validity or uid_validity != current_uid_validity:
|
||||||
# uidvalidity changed & all email uids are reindexed by server
|
# uidvalidity changed & all email uids are reindexed by server
|
||||||
frappe.db.sql(
|
Communication = frappe.qb.DocType("Communication")
|
||||||
"""update `tabCommunication` set uid=-1 where communication_medium='Email'
|
frappe.qb.update(Communication) \
|
||||||
and email_account=%s""", (self.settings.email_account,)
|
.set(Communication.uid, -1) \
|
||||||
)
|
.where(Communication.communication_medium == "Email") \
|
||||||
frappe.db.sql(
|
.where(Communication.email_account == self.settings.email_account).run()
|
||||||
"""update `tabEmail Account` set uidvalidity=%s, uidnext=%s where
|
|
||||||
name=%s""", (current_uid_validity, uidnext, self.settings.email_account)
|
if self.settings.use_imap:
|
||||||
)
|
# new update for the IMAP Folder DocType
|
||||||
|
IMAPFolder = frappe.qb.DocType("IMAP Folder")
|
||||||
|
frappe.qb.update(IMAPFolder) \
|
||||||
|
.set(IMAPFolder.uidvalidity, current_uid_validity) \
|
||||||
|
.set(IMAPFolder.uidnext, uidnext) \
|
||||||
|
.where(IMAPFolder.parent == self.settings.email_account_name) \
|
||||||
|
.where(IMAPFolder.folder_name == folder).run()
|
||||||
|
else:
|
||||||
|
EmailAccount = frappe.qb.DocType("Email Account")
|
||||||
|
frappe.qb.update(EmailAccount) \
|
||||||
|
.set(EmailAccount.uidvalidity, current_uid_validity) \
|
||||||
|
.set(EmailAccount.uidnext, uidnext) \
|
||||||
|
.where(EmailAccount.name == self.settings.email_account_name).run()
|
||||||
|
|
||||||
# uid validity not found pulling emails for first time
|
# uid validity not found pulling emails for first time
|
||||||
if not uid_validity:
|
if not uid_validity:
|
||||||
|
|
@ -232,6 +248,7 @@ class EmailServer:
|
||||||
def parse_imap_response(self, cmd, response):
|
def parse_imap_response(self, cmd, response):
|
||||||
pattern = r"(?<={cmd} )[0-9]*".format(cmd=cmd)
|
pattern = r"(?<={cmd} )[0-9]*".format(cmd=cmd)
|
||||||
match = re.search(pattern, response.decode('utf-8'), re.U | re.I)
|
match = re.search(pattern, response.decode('utf-8'), re.U | re.I)
|
||||||
|
|
||||||
if match:
|
if match:
|
||||||
return match.group(0)
|
return match.group(0)
|
||||||
else:
|
else:
|
||||||
|
|
@ -340,16 +357,15 @@ class EmailServer:
|
||||||
|
|
||||||
return error_msg
|
return error_msg
|
||||||
|
|
||||||
def update_flag(self, uid_list=None):
|
def update_flag(self, folder, uid_list=None):
|
||||||
""" set all uids mails the flag as seen """
|
""" set all uids mails the flag as seen """
|
||||||
|
|
||||||
if not uid_list:
|
if not uid_list:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.connect():
|
if not self.connect():
|
||||||
return
|
return
|
||||||
|
|
||||||
self.imap.select("Inbox")
|
self.imap.select(folder)
|
||||||
for uid, operation in uid_list.items():
|
for uid, operation in uid_list.items():
|
||||||
if not uid: continue
|
if not uid: continue
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,12 @@ class TestSMTP(unittest.TestCase):
|
||||||
make_server(port, 0, 1)
|
make_server(port, 0, 1)
|
||||||
|
|
||||||
def test_get_email_account(self):
|
def test_get_email_account(self):
|
||||||
existing_email_accounts = frappe.get_all("Email Account", fields = ["name", "enable_outgoing", "default_outgoing", "append_to"])
|
existing_email_accounts = frappe.get_all("Email Account", fields = ["name", "enable_outgoing", "default_outgoing","append_to", "use_imap"])
|
||||||
unset_details = {
|
unset_details = {
|
||||||
"enable_outgoing": 0,
|
"enable_outgoing": 0,
|
||||||
"default_outgoing": 0,
|
"default_outgoing": 0,
|
||||||
"append_to": None
|
"append_to": None,
|
||||||
|
"use_imap": 0
|
||||||
}
|
}
|
||||||
for email_account in existing_email_accounts:
|
for email_account in existing_email_accounts:
|
||||||
frappe.db.set_value('Email Account', email_account['name'], unset_details)
|
frappe.db.set_value('Email Account', email_account['name'], unset_details)
|
||||||
|
|
@ -60,7 +61,8 @@ def create_email_account(email_id, password, enable_outgoing, default_outgoing=0
|
||||||
"enable_incoming": 1,
|
"enable_incoming": 1,
|
||||||
"append_to":append_to,
|
"append_to":append_to,
|
||||||
"is_dummy_password": 1,
|
"is_dummy_password": 1,
|
||||||
"smtp_server": "localhost"
|
"smtp_server": "localhost",
|
||||||
|
"use_imap": 0
|
||||||
}
|
}
|
||||||
|
|
||||||
email_account = frappe.new_doc('Email Account')
|
email_account = frappe.new_doc('Email Account')
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,11 @@ class EventProducer(Document):
|
||||||
self.db_set('incoming_change', 0)
|
self.db_set('incoming_change', 0)
|
||||||
self.reload()
|
self.reload()
|
||||||
|
|
||||||
|
def on_trash(self):
|
||||||
|
last_update = frappe.db.get_value('Event Producer Last Update', dict(event_producer=self.name))
|
||||||
|
if last_update:
|
||||||
|
frappe.delete_doc('Event Producer Last Update', last_update)
|
||||||
|
|
||||||
def check_url(self):
|
def check_url(self):
|
||||||
valid_url_schemes = ("http", "https")
|
valid_url_schemes = ("http", "https")
|
||||||
frappe.utils.validate_url(self.producer_url, throw=True, valid_schemes=valid_url_schemes)
|
frappe.utils.validate_url(self.producer_url, throw=True, valid_schemes=valid_url_schemes)
|
||||||
|
|
|
||||||
|
|
@ -255,7 +255,7 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
|
||||||
response = doc.run_method(method, **args)
|
response = doc.run_method(method, **args)
|
||||||
|
|
||||||
frappe.response.docs.append(doc)
|
frappe.response.docs.append(doc)
|
||||||
if not response:
|
if response is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# build output as csv
|
# build output as csv
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,8 @@ scheduler_events = {
|
||||||
"frappe.automation.doctype.auto_repeat.auto_repeat.set_auto_repeat_as_completed",
|
"frappe.automation.doctype.auto_repeat.auto_repeat.set_auto_repeat_as_completed",
|
||||||
"frappe.email.doctype.unhandled_email.unhandled_email.remove_old_unhandled_emails",
|
"frappe.email.doctype.unhandled_email.unhandled_email.remove_old_unhandled_emails",
|
||||||
"frappe.core.doctype.prepared_report.prepared_report.delete_expired_prepared_reports",
|
"frappe.core.doctype.prepared_report.prepared_report.delete_expired_prepared_reports",
|
||||||
"frappe.core.doctype.log_settings.log_settings.run_log_clean_up"
|
"frappe.core.doctype.log_settings.log_settings.run_log_clean_up",
|
||||||
|
"frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.process_data_deletion_request"
|
||||||
],
|
],
|
||||||
"daily_long": [
|
"daily_long": [
|
||||||
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",
|
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",
|
||||||
|
|
|
||||||
|
|
@ -324,7 +324,7 @@ def _delete_doctypes(doctypes: List[str], dry_run: bool) -> None:
|
||||||
print(f"* dropping Table for '{doctype}'...")
|
print(f"* dropping Table for '{doctype}'...")
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
frappe.delete_doc("DocType", doctype, ignore_on_trash=True)
|
frappe.delete_doc("DocType", doctype, ignore_on_trash=True)
|
||||||
frappe.db.sql_ddl(f"drop table `tab{doctype}`")
|
frappe.db.sql_ddl(f"DROP TABLE IF EXISTS `tab{doctype}`")
|
||||||
|
|
||||||
|
|
||||||
def post_install(rebuild_website=False):
|
def post_install(rebuild_website=False):
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,11 @@ data_fieldtypes = (
|
||||||
'Icon'
|
'Icon'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
attachment_fieldtypes = (
|
||||||
|
'Attach',
|
||||||
|
'Attach Image',
|
||||||
|
)
|
||||||
|
|
||||||
no_value_fields = (
|
no_value_fields = (
|
||||||
'Section Break',
|
'Section Break',
|
||||||
'Column Break',
|
'Column Break',
|
||||||
|
|
|
||||||
|
|
@ -338,7 +338,7 @@ class BaseDocument(object):
|
||||||
return self.meta.get_field(fieldname).options
|
return self.meta.get_field(fieldname).options
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
if self.doctype == 'DocType':
|
if self.doctype == 'DocType':
|
||||||
return dict(links='DocType Link', actions='DocType Action').get(fieldname)
|
return dict(links='DocType Link', actions='DocType Action', states='DocType State').get(fieldname)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def get_parentfield_of_doctype(self, doctype):
|
def get_parentfield_of_doctype(self, doctype):
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue