diff --git a/frappe/__init__.py b/frappe/__init__.py index ea7432f562..d00a3e64e6 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -309,7 +309,7 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message as_markdown=False, bulk=False, reference_doctype=None, reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, attachments=None, content=None, doctype=None, name=None, reply_to=None, - cc=(), message_id=None, as_bulk=False, send_after=None): + cc=(), message_id=None, as_bulk=False, send_after=None, expose_recipients=False): """Send email using user's default **Email Account** or global default **Email Account**. @@ -327,6 +327,7 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message :param reply_to: Reply-To email id. :param message_id: Used for threading. If a reply is received to this email, Message-Id is sent back as In-Reply-To in received email. :param send_after: Send after the given datetime. + :param expose_recipients: Display all recipients in the footer message - "This email was sent to" """ if bulk or as_bulk: @@ -335,7 +336,8 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message subject=subject, message=content or message, reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message, - attachments=attachments, reply_to=reply_to, cc=cc, message_id=message_id, send_after=send_after) + attachments=attachments, reply_to=reply_to, cc=cc, message_id=message_id, send_after=send_after, + expose_recipients=expose_recipients) else: import frappe.email if as_markdown: diff --git a/frappe/__version__.py b/frappe/__version__.py index 2a69af1984..243aeb18c1 100644 --- a/frappe/__version__.py +++ b/frappe/__version__.py @@ -1,2 +1,2 @@ from __future__ import unicode_literals -__version__ = "6.2.0" +__version__ = "6.3.0" diff --git a/frappe/async.py b/frappe/async.py index 088407e287..f4539ecb48 100644 --- a/frappe/async.py +++ b/frappe/async.py @@ -17,57 +17,38 @@ END_LINE = '' TASK_LOG_MAX_AGE = 86400 # 1 day in seconds redis_server = None - def handler(f): cmd = f.__module__ + '.' + f.__name__ - def _run(args, set_in_response=True): + def run(args, set_in_response=True): from frappe.tasks import run_async_task from frappe.handler import execute_cmd if frappe.conf.disable_async: return execute_cmd(cmd, from_async=True) args = frappe._dict(args) - task = run_async_task.delay(frappe.local.site, - (frappe.session and frappe.session.user) or 'Administrator', cmd, args) + task = run_async_task.delay(site=frappe.local.site, + user=(frappe.session and frappe.session.user) or 'Administrator', cmd=cmd, + form_dict=args) if set_in_response: frappe.local.response['task_id'] = task.id return task.id @wraps(f) def queue(*args, **kwargs): - from frappe.tasks import run_async_task - from frappe.handler import execute_cmd - if frappe.conf.disable_async: - return execute_cmd(cmd, from_async=True) - task = run_async_task.delay(frappe.local.site, - (frappe.session and frappe.session.user) or 'Administrator', cmd, - frappe.local.form_dict) - frappe.local.response['task_id'] = task.id + task_id = run(frappe.local.form_dict, set_in_response=True) return { "status": "queued", - "task_id": task.id + "task_id": task_id } + queue.async = True queue.queue = f - queue.run = _run + queue.run = run frappe.whitelisted.append(f) frappe.whitelisted.append(queue) return queue -def run_async_task(method, args, reference_doctype=None, reference_name=None, set_in_response=True): - if frappe.local.request and frappe.local.request.method == "GET": - frappe.throw("Cannot run task in a GET request") - task_id = method.run(args, set_in_response=set_in_response) - task = frappe.new_doc("Async Task") - task.celery_task_id = task_id - task.status = "Queued" - task.reference_doctype = reference_doctype - task.reference_name = reference_name - task.save() - return task_id - - @frappe.whitelist() def get_pending_tasks_for_doc(doctype, docname): return frappe.db.sql_list("select name from `tabAsync Task` where status in ('Queued', 'Running') and reference_doctype='%s' and reference_name='%s'" % (doctype, docname)) @@ -76,10 +57,9 @@ def get_pending_tasks_for_doc(doctype, docname): @handler def ping(): from time import sleep - sleep(6) + sleep(1) return "pong" - @frappe.whitelist() def get_task_status(task_id): from frappe.celery_app import get_celery @@ -91,9 +71,7 @@ def get_task_status(task_id): "progress": 0 } - def set_task_status(task_id, status, response=None): - frappe.db.set_value("Async Task", task_id, "status", status) if not response: response = {} response.update({ @@ -167,6 +145,7 @@ def emit_via_redis(event, message, room): try: r.publish('events', frappe.as_json({'event': event, 'message': message, 'room': room})) except redis.exceptions.ConnectionError: + # print frappe.get_traceback() pass def put_log(line_no, line, task_id=None): diff --git a/frappe/build.py b/frappe/build.py index d65efc538a..b91f8e85e4 100644 --- a/frappe/build.py +++ b/frappe/build.py @@ -174,6 +174,10 @@ def files_dirty(): return False def compile_less(): + from distutils.spawn import find_executable + if not find_executable("lessc"): + return + for path in app_paths: less_path = os.path.join(path, "public", "less") if os.path.exists(less_path): @@ -189,4 +193,4 @@ def compile_less(): print "compiling {0}".format(fpath) css_path = os.path.join(path, "public", "css", fname.rsplit(".", 1)[0] + ".css") - os.system("which lessc && lessc {0} > {1}".format(fpath, css_path)) + os.system("lessc {0} > {1}".format(fpath, css_path)) diff --git a/frappe/celery_app.py b/frappe/celery_app.py index 1165898c66..45eb8a974f 100644 --- a/frappe/celery_app.py +++ b/frappe/celery_app.py @@ -10,8 +10,9 @@ task_logger = get_task_logger(__name__) from datetime import timedelta import frappe -import json import os +import threading +import time SITES_PATH = os.environ.get('SITES_PATH', '.') @@ -26,35 +27,43 @@ _app = None def get_celery(): global _app if not _app: - conf = frappe.get_site_config(sites_path=SITES_PATH) - - _app = Celery('frappe', - broker=conf.celery_broker or DEFAULT_CELERY_BROKER, - backend=conf.async_redis_server or DEFAULT_CELERY_BACKEND) - - setup_celery(_app, conf) - + _app = get_celery_app() return _app - -def setup_celery(app, conf): + +def get_celery_app(): + conf = get_site_config() + app = Celery('frappe', + broker=conf.celery_broker or DEFAULT_CELERY_BROKER, + backend=conf.async_redis_server or DEFAULT_CELERY_BACKEND) + app.autodiscover_tasks(frappe.get_all_apps(with_frappe=True, with_internal_apps=False, sites_path=SITES_PATH)) + app.conf.CELERY_TASK_SERIALIZER = 'json' app.conf.CELERY_ACCEPT_CONTENT = ['json'] app.conf.CELERY_TIMEZONE = 'UTC' app.conf.CELERY_RESULT_SERIALIZER = 'json' - app.CELERY_TASK_RESULT_EXPIRES = timedelta(0, 3600) - + app.conf.CELERY_TASK_RESULT_EXPIRES = timedelta(0, 3600) + + if conf.monitory_celery: + app.conf.CELERY_SEND_EVENTS = True + app.conf.CELERY_SEND_TASK_SENT_EVENT = True + if conf.celery_queue_per_site: app.conf.CELERY_ROUTES = (SiteRouter(), AsyncTaskRouter()) - + app.conf.CELERYBEAT_SCHEDULE = get_beat_schedule(conf) if conf.celery_error_emails: app.conf.CELERY_SEND_TASK_ERROR_EMAILS = True for k, v in conf.celery_error_emails.iteritems(): setattr(app.conf, k, v) - + + return app + +def get_site_config(): + return frappe.get_site_config(sites_path=SITES_PATH) + class SiteRouter(object): def route_for_task(self, task, args=None, kwargs=None): if hasattr(frappe.local, 'site'): @@ -62,17 +71,17 @@ class SiteRouter(object): return get_queue(frappe.local.site, LONGJOBS_PREFIX) else: return get_queue(frappe.local.site) - + return None class AsyncTaskRouter(object): def route_for_task(self, task, args=None, kwargs=None): if task == "frappe.tasks.run_async_task" and hasattr(frappe.local, 'site'): return get_queue(frappe.local.site, ASYNC_TASKS_PREFIX) - + def get_queue(site, prefix=None): return {'queue': "{}{}".format(prefix or "", site)} - + def get_beat_schedule(conf): schedule = { 'scheduler': { @@ -80,17 +89,136 @@ def get_beat_schedule(conf): 'schedule': timedelta(seconds=conf.scheduler_interval or DEFAULT_SCHEDULER_INTERVAL) }, } - + if conf.celery_queue_per_site: schedule['sync_queues'] = { 'task': 'frappe.tasks.sync_queues', 'schedule': timedelta(seconds=conf.scheduler_interval or DEFAULT_SCHEDULER_INTERVAL) } - + return schedule - + def celery_task(*args, **kwargs): return get_celery().task(*args, **kwargs) - + +def make_async_task(args): + task = frappe.new_doc("Async Task") + task.update(args) + task.status = "Queued" + task.set_docstatus_user_and_timestamp() + task.db_insert() + task.notify_update() + +def run_test(): + for i in xrange(30): + test.delay(site=frappe.local.site) + +@celery_task() +def test(site=None): + time.sleep(1) + print "task" + +class MonitorThread(object): + """Thread manager for monitoring celery events""" + def __init__(self, celery_app, interval=1): + self.celery_app = celery_app + self.interval = interval + + self.state = self.celery_app.events.State() + + self.thread = threading.Thread(target=self.run, args=()) + self.thread.daemon = True + self.thread.start() + + def catchall(self, event): + if event['type'] != 'worker-heartbeat': + self.state.event(event) + + if not 'uuid' in event: + return + + task = self.state.tasks.get(event['uuid']) + info = task.info() + + if 'name' in event and 'enqueue_events_for_site' in event['name']: + return + + try: + kwargs = eval(info.get('kwargs')) + + if 'site' in kwargs: + frappe.connect(kwargs['site']) + + if event['type']=='task-sent': + make_async_task({ + 'name': event['uuid'], + 'task_name': kwargs.get("cmd") or event['name'] + }) + + elif event['type']=='task-received': + try: + task = frappe.get_doc("Async Task", event['uuid']) + task.status = 'Started' + task.set_docstatus_user_and_timestamp() + task.db_update() + task.notify_update() + except frappe.DoesNotExistError: + pass + + elif event['type']=='task-succeeded': + try: + task = frappe.get_doc("Async Task", event['uuid']) + task.status = 'Succeeded' + task.result = info.get('result') + task.runtime = info.get('runtime') + task.set_docstatus_user_and_timestamp() + task.db_update() + task.notify_update() + except frappe.DoesNotExistError: + pass + + elif event['type']=='task-failed': + try: + task = frappe.get_doc("Async Task", event['uuid']) + task.status = 'Failed' + task.traceback = event.get('traceback') or event.get('exception') + task.traceback = frappe.as_json(info) + "\n\n" + task.traceback + task.runtime = info.get('runtime') + task.set_docstatus_user_and_timestamp() + task.db_update() + task.notify_update() + except frappe.DoesNotExistError: + pass + + frappe.db.commit() + except Exception: + print frappe.get_traceback() + finally: + frappe.destroy() + + + def run(self): + while True: + try: + with self.celery_app.connection() as connection: + recv = self.celery_app.events.Receiver(connection, handlers={ + '*': self.catchall + }) + recv.capture(limit=None, timeout=None, wakeup=True) + + except (KeyboardInterrupt, SystemExit): + raise + + except Exception: + # unable to capture + print "unable to capture:" + print frappe.get_traceback() + + time.sleep(self.interval) + + if __name__ == '__main__': - get_celery().start() + app = get_celery() + if get_site_config().get("monitor_celery"): + MonitorThread(app) + app.start() diff --git a/frappe/change_log/v6/v6_3_0.md b/frappe/change_log/v6/v6_3_0.md new file mode 100644 index 0000000000..f605c4607a --- /dev/null +++ b/frappe/change_log/v6/v6_3_0.md @@ -0,0 +1,2 @@ +- You can now add **CC** in Email +- Show checkboxes in Print diff --git a/frappe/core/doctype/async_task/async_task.json b/frappe/core/doctype/async_task/async_task.json index 2223088826..330596ccd2 100644 --- a/frappe/core/doctype/async_task/async_task.json +++ b/frappe/core/doctype/async_task/async_task.json @@ -2,7 +2,7 @@ "allow_copy": 0, "allow_import": 0, "allow_rename": 0, - "autoname": "field:celery_task_id", + "autoname": "", "creation": "2015-07-03 11:28:03.496346", "custom": 0, "docstatus": 0, @@ -13,18 +13,63 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "celery_task_id", + "fieldname": "status", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Status", + "no_copy": 0, + "options": "\nQueued\nRunning\nSucceeded\nFailed\n", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "task_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Task Name", + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "runtime", "fieldtype": "Data", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 1, - "label": "Celery Task ID", + "label": "Runtime", "no_copy": 0, "permlevel": 0, "precision": "", "print_hide": 0, - "read_only": 0, + "read_only": 1, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -35,19 +80,18 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "status", - "fieldtype": "Select", + "fieldname": "result", + "fieldtype": "Code", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, - "label": "Status", + "label": "Result", "no_copy": 0, - "options": "\nQueued\nRunning\nFinished\nFailed\n", "permlevel": 0, "precision": "", "print_hide": 0, - "read_only": 0, + "read_only": 1, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -58,13 +102,13 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "stdout", - "fieldtype": "Long Text", + "fieldname": "traceback", + "fieldtype": "Code", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, - "label": "stdout", + "label": "Traceback", "no_copy": 0, "permlevel": 0, "precision": "", @@ -80,18 +124,17 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "stderr", - "fieldtype": "Long Text", + "fieldname": "section_break_6", + "fieldtype": "Section Break", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, - "label": "stderr", "no_copy": 0, "permlevel": 0, "precision": "", "print_hide": 0, - "read_only": 1, + "read_only": 0, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -114,7 +157,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, - "read_only": 0, + "read_only": 1, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -137,7 +180,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, - "read_only": 0, + "read_only": 1, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -152,7 +195,7 @@ "is_submittable": 0, "issingle": 0, "istable": 0, - "modified": "2015-07-28 16:18:11.344349", + "modified": "2015-09-07 08:08:22.193911", "modified_by": "Administrator", "module": "Core", "name": "Async Task", @@ -183,5 +226,6 @@ "read_only": 0, "read_only_onload": 0, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "title_field": "task_name" } \ No newline at end of file diff --git a/frappe/core/doctype/async_task/async_task_list.js b/frappe/core/doctype/async_task/async_task_list.js new file mode 100644 index 0000000000..699da13084 --- /dev/null +++ b/frappe/core/doctype/async_task/async_task_list.js @@ -0,0 +1,10 @@ +frappe.listview_settings['Async Task'] = { + add_fields: ["status"], + get_indicator: function(doc) { + if(doc.status==="Succeeded") { + return [__("Succeeded"), "green", "status,=,Succeeded"]; + } else if(doc.status==="Failed") { + return [__("Failed"), "red", "status,=,Failed"]; + } + } +}; diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py index cca70fc6b4..6d798de258 100644 --- a/frappe/core/doctype/comment/comment.py +++ b/frappe/core/doctype/comment/comment.py @@ -42,7 +42,8 @@ class Comment(Document): message['broadcast'] = True frappe.publish_realtime('new_message', message) else: - frappe.publish_realtime('new_message', self.as_dict(), user=frappe.session.user) + # comment_docname contains the user who is addressed in the messages' page comment + frappe.publish_realtime('new_message', self.as_dict(), user=self.comment_docname) else: frappe.publish_realtime('new_comment', self.as_dict(), doctype= self.comment_doctype, docname = self.comment_docname) diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json index 31b263e9c5..16627d67fa 100644 --- a/frappe/core/doctype/communication/communication.json +++ b/frappe/core/doctype/communication/communication.json @@ -37,20 +37,21 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "sent_or_received", + "depends_on": "", + "fieldname": "communication_medium", "fieldtype": "Select", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 1, - "label": "Sent or Received", + "label": "Communication Medium", "no_copy": 0, - "options": "Sent\nReceived", + "options": "\nChat\nPhone\nEmail\nSMS\nVisit\nOther", "permlevel": 0, "print_hide": 0, "read_only": 0, "report_hide": 0, - "reqd": 1, + "reqd": 0, "search_index": 0, "set_only_once": 0, "unique": 0 @@ -59,17 +60,15 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "status", - "fieldtype": "Select", + "fieldname": "recipients", + "fieldtype": "Data", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, - "in_list_view": 1, - "label": "Status", + "in_list_view": 0, + "label": "Recipients", "no_copy": 0, - "options": "Open\nReplied\nClosed\nLinked", "permlevel": 0, - "precision": "", "print_hide": 0, "read_only": 0, "report_hide": 0, @@ -82,16 +81,15 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "description": "Integrations can use this field to set email delivery status", - "fieldname": "delivery_status", - "fieldtype": "Select", - "hidden": 1, + "depends_on": "eval:doc.communication_medium===\"Email\"", + "fieldname": "cc", + "fieldtype": "Data", + "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, - "label": "Delivery Status", + "label": "CC", "no_copy": 0, - "options": "\nSent\nBounced\nOpened\nMarked As Spam\nRejected\nDelayed\nSoft-Bounced\nClicked\nRecipient Unsubscribed", "permlevel": 0, "precision": "", "print_hide": 0, @@ -106,19 +104,20 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "subject", + "depends_on": "eval:doc.communication_medium!==\"Email\"", + "fieldname": "phone_no", "fieldtype": "Data", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, - "label": "Subject", + "label": "Phone No.", "no_copy": 0, "permlevel": 0, "print_hide": 0, "read_only": 0, "report_hide": 0, - "reqd": 1, + "reqd": 0, "search_index": 0, "set_only_once": 0, "unique": 0 @@ -148,15 +147,15 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "reference_doctype", - "fieldtype": "Link", + "fieldname": "status", + "fieldtype": "Select", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, - "in_list_view": 0, - "label": "Reference DocType", + "in_list_view": 1, + "label": "Status", "no_copy": 0, - "options": "DocType", + "options": "Open\nReplied\nClosed\nLinked", "permlevel": 0, "precision": "", "print_hide": 0, @@ -171,21 +170,20 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "reference_name", - "fieldtype": "Dynamic Link", + "fieldname": "sent_or_received", + "fieldtype": "Select", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, - "in_list_view": 0, - "label": "Reference Name", + "in_list_view": 1, + "label": "Sent or Received", "no_copy": 0, - "options": "reference_doctype", + "options": "Sent\nReceived", "permlevel": 0, - "precision": "", "print_hide": 0, "read_only": 0, "report_hide": 0, - "reqd": 0, + "reqd": 1, "search_index": 0, "set_only_once": 0, "unique": 0 @@ -194,13 +192,16 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "section_break_8", - "fieldtype": "Section Break", - "hidden": 0, + "description": "Integrations can use this field to set email delivery status", + "fieldname": "delivery_status", + "fieldtype": "Select", + "hidden": 1, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, + "label": "Delivery Status", "no_copy": 0, + "options": "\nSent\nBounced\nOpened\nMarked As Spam\nRejected\nDelayed\nSoft-Bounced\nClicked\nRecipient Unsubscribed", "permlevel": 0, "precision": "", "print_hide": 0, @@ -215,37 +216,15 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "content", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Content", - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, - "width": "400" - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "additional_info", + "fieldname": "section_break_10", "fieldtype": "Section Break", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, - "label": "Additional Info", "no_copy": 0, "permlevel": 0, + "precision": "", "print_hide": 0, "read_only": 0, "report_hide": 0, @@ -258,19 +237,19 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "recipients", + "fieldname": "subject", "fieldtype": "Data", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, - "label": "Recipients", + "label": "Subject", "no_copy": 0, "permlevel": 0, "print_hide": 0, "read_only": 0, "report_hide": 0, - "reqd": 0, + "reqd": 1, "search_index": 0, "set_only_once": 0, "unique": 0 @@ -279,15 +258,15 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "phone_no", - "fieldtype": "Data", + "fieldname": "section_break_8", + "fieldtype": "Section Break", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, - "label": "Phone No.", "no_copy": 0, "permlevel": 0, + "precision": "", "print_hide": 0, "read_only": 0, "report_hide": 0, @@ -300,15 +279,14 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "communication_medium", - "fieldtype": "Select", + "fieldname": "content", + "fieldtype": "Text Editor", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, - "in_list_view": 1, - "label": "Communication Medium", + "in_list_view": 0, + "label": "Content", "no_copy": 0, - "options": "\nChat\nPhone\nEmail\nSMS\nVisit\nOther", "permlevel": 0, "print_hide": 0, "read_only": 0, @@ -316,21 +294,22 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, - "unique": 0 + "unique": 0, + "width": "400" }, { "allow_on_submit": 0, "bold": 0, - "collapsible": 0, - "fieldname": "column_break_14", - "fieldtype": "Column Break", + "collapsible": 1, + "fieldname": "additional_info", + "fieldtype": "Section Break", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, + "label": "More Information", "no_copy": 0, "permlevel": 0, - "precision": "", "print_hide": 0, "read_only": 0, "report_hide": 0, @@ -386,14 +365,15 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "section_break2", - "fieldtype": "Section Break", + "default": "Today", + "fieldname": "communication_date", + "fieldtype": "Datetime", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, + "label": "Date", "no_copy": 0, - "options": "simple", "permlevel": 0, "print_hide": 0, "read_only": 0, @@ -407,15 +387,15 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "column_break4", + "fieldname": "column_break_14", "fieldtype": "Column Break", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, - "label": "By", "no_copy": 0, "permlevel": 0, + "precision": "", "print_hide": 0, "read_only": 0, "report_hide": 0, @@ -428,15 +408,15 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "email_account", + "fieldname": "reference_doctype", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, - "label": "Email Account", + "label": "Reference DocType", "no_copy": 0, - "options": "Email Account", + "options": "DocType", "permlevel": 0, "precision": "", "print_hide": 0, @@ -451,19 +431,19 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "default": "__user", - "fieldname": "user", - "fieldtype": "Link", + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", "hidden": 0, - "ignore_user_permissions": 1, + "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, - "label": "User", + "label": "Reference Name", "no_copy": 0, - "options": "User", + "options": "reference_doctype", "permlevel": 0, + "precision": "", "print_hide": 0, - "read_only": 1, + "read_only": 0, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -474,17 +454,19 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "column_break5", - "fieldtype": "Column Break", + "fieldname": "in_reply_to", + "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, - "label": "On", + "label": "In Reply To", "no_copy": 0, + "options": "Communication", "permlevel": 0, + "precision": "", "print_hide": 0, - "read_only": 0, + "read_only": 1, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -495,16 +477,17 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "default": "Today", - "fieldname": "communication_date", - "fieldtype": "Datetime", + "fieldname": "email_account", + "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, "in_list_view": 0, - "label": "Date", + "label": "Email Account", "no_copy": 0, + "options": "Email Account", "permlevel": 0, + "precision": "", "print_hide": 0, "read_only": 0, "report_hide": 0, @@ -517,17 +500,19 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "_user_tags", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, + "default": "__user", + "fieldname": "user", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 1, "in_filter": 0, "in_list_view": 0, - "label": "User Tags", - "no_copy": 1, + "label": "User", + "no_copy": 0, + "options": "User", "permlevel": 0, - "print_hide": 1, - "read_only": 0, + "print_hide": 0, + "read_only": 1, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -556,6 +541,27 @@ "search_index": 0, "set_only_once": 0, "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "_user_tags", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "User Tags", + "no_copy": 1, + "permlevel": 0, + "print_hide": 1, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "hide_heading": 0, @@ -567,7 +573,7 @@ "is_submittable": 0, "issingle": 0, "istable": 0, - "modified": "2015-08-14 17:46:20.902296", + "modified": "2015-09-15 05:51:16.112080", "modified_by": "Administrator", "module": "Core", "name": "Communication", diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 06f518f7bc..08cd37df2a 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals, absolute_import import frappe import json from email.utils import formataddr, parseaddr -from frappe.utils import get_url, get_formatted_email, cstr, cint +from frappe.utils import get_url, get_formatted_email, cstr, cint, validate_email_add, split_emails from frappe.utils.file_manager import get_file import frappe.email.smtp from frappe import _ @@ -30,10 +30,27 @@ class Communication(Document): if self.get("__islocal"): if self.reference_doctype and self.reference_name: self.status = "Linked" - else: self.status = "Open" + # validate recipients + for email in split_emails(self.recipients): + validate_email_add(email, throw=True) + + # validate CC + for email in split_emails(self.cc): + validate_email_add(email, throw=True) + + def after_insert(self): + # send new comment to listening clients + comment = self.as_dict() + comment["comment"] = comment["content"] + comment["comment_by"] = comment["sender"] + comment["comment_type"] = comment["communication_medium"] + + frappe.publish_realtime('new_comment', comment, doctype = self.reference_doctype, + docname = self.reference_name) + def on_update(self): """Update parent status as `Open` or `Replied`.""" self.update_parent() @@ -50,9 +67,9 @@ class Communication(Document): to_status = "Open" if self.sent_or_received=="Received" else "Replied" if to_status in status_field.options.splitlines(): - frappe.db.set_value(parent.doctype, parent.name, "status", to_status) + parent.db_set("status", to_status) - parent.notify_modified() + parent.notify_update() def send(self, print_html=None, print_format=None, attachments=None, send_me_a_copy=False, recipients=None): @@ -64,51 +81,41 @@ class Communication(Document): self.send_me_a_copy = send_me_a_copy self.notify(print_html, print_format, attachments, recipients) - def set_incoming_outgoing_accounts(self): - self.incoming_email_account = self.outgoing_email_account = None - - if self.reference_doctype: - self.incoming_email_account = frappe.db.get_value("Email Account", - {"append_to": self.reference_doctype, "enable_incoming": 1}, "email_id") - - self.outgoing_email_account = frappe.db.get_value("Email Account", - {"append_to": self.reference_doctype, "enable_outgoing": 1}, - ["email_id", "always_use_account_email_id_as_sender"], as_dict=True) - - if not self.incoming_email_account: - self.incoming_email_account = frappe.db.get_value("Email Account", {"default_incoming": 1}, "email_id") - - if not self.outgoing_email_account: - self.outgoing_email_account = frappe.db.get_value("Email Account", {"default_outgoing": 1}, - ["email_id", "always_use_account_email_id_as_sender"], as_dict=True) or frappe._dict() - - def notify(self, print_html=None, print_format=None, attachments=None, recipients=None, except_recipient=False): + def notify(self, print_html=None, print_format=None, attachments=None, + recipients=None, cc=None, fetched_from_email_account=False): """Calls a delayed celery task 'sendmail' that enqueus email in Bulk Email queue :param print_html: Send given value as HTML attachment :param print_format: Attach print format of parent document :param attachments: A list of filenames that should be attached when sending this email :param recipients: Email recipients - :param except_recipient: True when pulling email, the notification shouldn't go to the main recipient + :param cc: Send email as CC to + :param fetched_from_email_account: True when pulling email, the notification shouldn't go to the main recipient """ + recipients, cc = self.get_recipients_and_cc(recipients, cc, + fetched_from_email_account=fetched_from_email_account) + + self.emails_not_sent_to = set(self.all_email_addresses) - set(recipients) - set(cc) + if frappe.flags.in_test: # for test cases, run synchronously self._notify(print_html=print_html, print_format=print_format, attachments=attachments, - recipients=recipients, except_recipient=except_recipient) + recipients=recipients, cc=cc) else: from frappe.tasks import sendmail sendmail.delay(frappe.local.site, self.name, print_html=print_html, print_format=print_format, attachments=attachments, - recipients=recipients, except_recipient=except_recipient) + recipients=recipients, cc=cc) + + def _notify(self, print_html=None, print_format=None, attachments=None, + recipients=None, cc=None): - def _notify(self, print_html=None, print_format=None, attachments=None, recipients=None, except_recipient=False): self.prepare_to_notify(print_html, print_format, attachments) - if not recipients: - recipients = self.get_recipients(except_recipient=except_recipient) frappe.sendmail( - recipients=recipients, + recipients=(recipients or []) + (cc or []), + expose_recipients=True, sender=self.sender, reply_to=self.incoming_email_account, subject=self.subject, @@ -121,6 +128,27 @@ class Communication(Document): bulk=True ) + def get_recipients_and_cc(self, recipients, cc, fetched_from_email_account=False): + self.all_email_addresses = [] + + if not recipients: + recipients = self.get_recipients() + + if not cc: + cc = self.get_cc(recipients, fetched_from_email_account=fetched_from_email_account) + + if fetched_from_email_account: + # email was already sent to the original recipient by the sender's email service + original_recipients, recipients = recipients, [] + + # cc that was received in the email + original_cc = split_emails(self.cc) + + # don't cc to people who already received the mail from sender's email service + cc = list(set(cc) - set(original_cc) - set(original_recipients)) + + return recipients, cc + def prepare_to_notify(self, print_html=None, print_format=None, attachments=None): """Prepare to make multipart MIME Email @@ -156,78 +184,129 @@ class Communication(Document): else: self.attachments.append(a) - def get_recipients(self, except_recipient=False): - """Build a list of users to which this email should go to""" + def set_incoming_outgoing_accounts(self): + self.incoming_email_account = self.outgoing_email_account = None + + if self.reference_doctype: + self.incoming_email_account = frappe.db.get_value("Email Account", + {"append_to": self.reference_doctype, "enable_incoming": 1}, "email_id") + + self.outgoing_email_account = frappe.db.get_value("Email Account", + {"append_to": self.reference_doctype, "enable_outgoing": 1}, + ["email_id", "always_use_account_email_id_as_sender"], as_dict=True) + + if not self.incoming_email_account: + self.incoming_email_account = frappe.db.get_value("Email Account", {"default_incoming": 1}, "email_id") + + if not self.outgoing_email_account: + self.outgoing_email_account = frappe.db.get_value("Email Account", {"default_outgoing": 1}, + ["email_id", "always_use_account_email_id_as_sender"], as_dict=True) or frappe._dict() + + def get_recipients(self): + """Build a list of email addresses for To""" # [EDGE CASE] self.recipients can be None when an email is sent as BCC - original_recipients = [s.strip() for s in cstr(self.recipients).split(",")] - recipients = original_recipients[:] + recipients = split_emails(self.recipients) + + if recipients: + # this will be used to eventually find email addresses that aren't sent to + self.all_email_addresses.extend(recipients) + + # exclude email accounts + exclude = [d[0] for d in + frappe.db.get_all("Email Account", ["email_id"], {"enable_incoming": 1}, as_list=True)] + exclude += [d[0] for d in + frappe.db.get_all("Email Account", ["login_id"], {"enable_incoming": 1}, as_list=True) + if d[0]] + + recipients = self.filter_email_list(recipients, exclude) + + return recipients + + def get_cc(self, recipients=None, fetched_from_email_account=False): + """Build a list of email addresses for CC""" + # get a copy of CC list + cc = split_emails(self.cc) if self.reference_doctype and self.reference_name: - recipients += self.get_earlier_participants() - recipients += self.get_commentors() - recipients += self.get_assignees() - recipients += self.get_starrers() + if not cc or fetched_from_email_account: + # if CC is not mentioned from the UI or is a fetched email, add follows to CC + cc.append(self.get_owner_email()) + cc += self.get_assignees() + cc += self.get_starrers() + + if fetched_from_email_account and self.in_reply_to: + # add sender of previous reply + cc.append(frappe.db.get_value("Communication", self.in_reply_to, "sender")) + + if cc: + # this will be used to eventually find email addresses that aren't sent to + self.all_email_addresses.extend(cc) + + # exclude email accounts, unfollows, recipients and unsubscribes + exclude = [d[0] for d in + frappe.db.get_all("Email Account", ["email_id"], {"enable_incoming": 1}, as_list=True)] + exclude += [d[0] for d in + frappe.db.get_all("Email Account", ["login_id"], {"enable_incoming": 1}, as_list=True) + if d[0]] + exclude += [d[0] for d in frappe.db.get_all("User", ["name"], {"thread_notify": 0}, as_list=True)] + exclude += [parseaddr(email)[1] for email in recipients] + + if fetched_from_email_account: + # exclude sender when pulling email + exclude += [parseaddr(self.sender)[1]] - # remove unsubscribed recipients - unsubscribed = [d[0] for d in frappe.db.get_all("User", ["name"], {"thread_notify": 0}, as_list=True)] - email_accounts = [d[0] for d in frappe.db.get_all("Email Account", ["email_id"], {"enable_incoming": 1}, as_list=True)] - sender = parseaddr(self.sender)[1] + if self.reference_doctype and self.reference_name: + exclude += [d[0] for d in frappe.db.get_all("Email Unsubscribe", ["email"], + {"reference_doctype": self.reference_doctype, "reference_name": self.reference_name}, as_list=True)] - filtered = [] - email_addresses = [] - for e in list(set(recipients)): - if (e=="Administrator") or ((e==self.sender) and (e not in original_recipients)) or \ - (e in unsubscribed) or (e in email_accounts): - continue + cc = self.filter_email_list(cc, exclude) + + if getattr(self, "send_me_a_copy", False) and self.sender not in cc: + self.all_email_addresses.append(self.sender) + cc.append(self.sender) - email_id = parseaddr(e)[1] + return cc + + def filter_email_list(self, email_list, exclude): + # temp variables + filtered = [] + email_address_list = [] - if not email_id: + for email in list(set(email_list)): + if email in exclude: continue - if email_id==sender or email_id in unsubscribed or email_id in email_accounts: + email_address = (parseaddr(email)[1] or "").lower() + if not email_address: continue - if except_recipient and (e==self.recipients or email_id==self.recipients): - # while pulling email, don't send email to current recipient + if email_address in exclude: continue # make sure of case-insensitive uniqueness of email address - if email_id.lower() not in email_addresses: + if email_address not in email_address_list: # append the full email i.e. "Human " - filtered.append(e) - email_addresses.append(email_id.lower()) - - if getattr(self, "send_me_a_copy", False): - filtered.append(self.sender) + filtered.append(email) + email_address_list.append(email_address) return filtered def get_starrers(self): """Return list of users who have starred this document.""" - if self.reference_doctype and self.reference_name: - return self.get_parent_doc().get_starred_by() - else: - return [] - - def get_earlier_participants(self): - return frappe.db.sql_list(""" - select distinct sender - from tabCommunication where - reference_doctype=%s and reference_name=%s""", - (self.reference_doctype, self.reference_name)) - - def get_commentors(self): - return frappe.db.sql_list(""" - select distinct comment_by - from tabComment where - comment_doctype=%s and comment_docname=%s and - ifnull(unsubscribed, 0)=0 and comment_by!='Administrator'""", - (self.reference_doctype, self.reference_name)) + return [( get_formatted_email(user) or user ) for user in self.get_parent_doc().get_starred_by()] + + def get_owner_email(self): + owner = self.get_parent_doc().owner + return get_formatted_email(owner) or owner def get_assignees(self): - return [d.owner for d in frappe.db.get_all("ToDo", filters={"reference_type": self.reference_doctype, - "reference_name": self.reference_name, "status": "Open"}, fields=["owner"])] + return [( get_formatted_email(d.owner) or d.owner ) for d in + frappe.db.get_all("ToDo", filters={ + "reference_type": self.reference_doctype, + "reference_name": self.reference_name, + "status": "Open" + }, fields=["owner"]) + ] def get_attach_link(self, print_format): """Returns public link for the attachment via `templates/emails/print_link.html`.""" @@ -247,7 +326,7 @@ def on_doctype_update(): def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent", sender=None, recipients=None, communication_medium="Email", send_email=False, print_html=None, print_format=None, attachments='[]', ignore_doctype_permissions=False, - send_me_a_copy=False): + send_me_a_copy=False, cc=None): """Make a new communication. :param doctype: Reference DocType. @@ -280,6 +359,7 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "content": content, "sender": sender, "recipients": recipients, + "cc": cc or None, "communication_medium": "Email", "sent_or_received": sent_or_received, "reference_doctype": doctype, @@ -291,15 +371,13 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = # if not committed, delayed task doesn't find the communication frappe.db.commit() - recipients = None if send_email: comm.send_me_a_copy = send_me_a_copy - recipients = comm.get_recipients() - comm.send(print_html, print_format, attachments, send_me_a_copy=send_me_a_copy, recipients=recipients) + comm.send(print_html, print_format, attachments, send_me_a_copy=send_me_a_copy) return { "name": comm.name, - "recipients": ", ".join(recipients) if recipients else None + "emails_not_sent_to": ", ".join(comm.emails_not_sent_to) if hasattr(comm, "emails_not_sent_to") else None } @frappe.whitelist() diff --git a/frappe/core/doctype/page/page.json b/frappe/core/doctype/page/page.json index f46b888d29..67aa6e62a4 100644 --- a/frappe/core/doctype/page/page.json +++ b/frappe/core/doctype/page/page.json @@ -64,7 +64,7 @@ "in_filter": 0, "in_list_view": 1, "label": "Title", - "no_copy": 0, + "no_copy": 1, "permlevel": 0, "print_hide": 0, "read_only": 0, @@ -217,7 +217,7 @@ "is_submittable": 0, "issingle": 0, "istable": 0, - "modified": "2015-07-13 04:45:55.942795", + "modified": "2015-09-11 12:19:55.121822", "modified_by": "Administrator", "module": "Core", "name": "Page", diff --git a/frappe/core/page/data_import_tool/data_import_tool.js b/frappe/core/page/data_import_tool/data_import_tool.js index 39d52422d2..bc3917ee59 100644 --- a/frappe/core/page/data_import_tool/data_import_tool.js +++ b/frappe/core/page/data_import_tool/data_import_tool.js @@ -75,20 +75,35 @@ frappe.DataImportTool = Class.extend({ onerror: function(r) { me.onerror(r); }, - start: function() { + queued: function() { + // async, show queued + msg_dialog.clear(); + msgprint(__("Import Request Queued. This may take a few moments, please be patient.")); + }, + running: function() { + // update async status as running + msg_dialog.clear(); + msgprint(__("Importing...")); me.write_messages([__("Importing")]); + me.has_progress = false; }, progress: function(data) { + // show callback if async if(data.progress) { frappe.hide_msgprint(true); - frappe.show_progress(__("Importing"), data.progress[0], data.progress[1]); + me.has_progress = true; + frappe.show_progress(__("Importing"), data.progress[0], + data.progress[1]); } }, callback: function(attachment, r) { if(r.message.error) { me.onerror(r); } else { - frappe.show_progress(__("Importing"), 1, 1); + if(me.has_progress) { + frappe.show_progress(__("Importing"), 1, 1); + setTimeout(frappe.hide_progress, 1000); + } r.messages = ["
" + __("Import Successful!") + "
"]. concat(r.message.messages) @@ -98,6 +113,15 @@ frappe.DataImportTool = Class.extend({ } }); + frappe.realtime.on("data_import_progress", function(data) { + if(data.progress) { + frappe.hide_msgprint(true); + me.has_progress = true; + frappe.show_progress(__("Importing"), data.progress[0], + data.progress[1]); + } + }) + }, write_messages: function(data) { this.page.main.find(".import-log").removeClass("hide"); diff --git a/frappe/core/page/data_import_tool/importer.py b/frappe/core/page/data_import_tool/importer.py index 5bd5d73a91..1edcf71ba9 100644 --- a/frappe/core/page/data_import_tool/importer.py +++ b/frappe/core/page/data_import_tool/importer.py @@ -16,7 +16,7 @@ from frappe.utils import cint, cstr, flt from frappe.core.page.data_import_tool.data_import_tool import get_data_keys #@frappe.async.handler -@frappe.whitelist() +frappe.whitelist() def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, overwrite=None, ignore_links=False, pre_process=None): """upload data""" @@ -203,11 +203,11 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, row_idx = i + start_row doc = None - frappe.publish_realtime(message = {"progress": [i, total]}) + # publish task_update + frappe.publish_realtime("data_import_progress", {"progress": [i, total]}, + user=frappe.session.user, now=True) try: - frappe.local.message_log = [] - doc = get_doc(row_idx) if pre_process: pre_process(doc) @@ -243,6 +243,8 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, ret.append('Error for row (#%d) %s : %s' % (row_idx + 1, len(row)>1 and row[1] or "", err_msg)) frappe.errprint(frappe.get_traceback()) + finally: + frappe.local.message_log = [] if error: frappe.db.rollback() diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js index 55b2965904..828639c124 100644 --- a/frappe/core/page/permission_manager/permission_manager.js +++ b/frappe/core/page/permission_manager/permission_manager.js @@ -503,13 +503,6 @@ frappe.PermissionEngine = Class.extend({ get_perm: function(name) { return $.map(this.perm_list, function(d) { if(d.name==name) return d; })[0]; }, - get_user_fields: function(doctype) { - var user_fields = frappe.get_children("DocType", doctype, "fields", {fieldtype:"Link", options:"User"}) - user_fields = user_fields.concat(frappe.get_children("DocType", doctype, "fields", - {fieldtype:"Select", link_doctype:"User"})) - - return user_fields - }, get_link_fields: function(doctype) { return frappe.get_children("DocType", doctype, "fields", {fieldtype:"Link", options:["not in", ["User", '[Select]']]}); diff --git a/frappe/desk/doctype/note/note.json b/frappe/desk/doctype/note/note.json index bba36ed669..11121a2434 100644 --- a/frappe/desk/doctype/note/note.json +++ b/frappe/desk/doctype/note/note.json @@ -20,7 +20,7 @@ "in_filter": 0, "in_list_view": 0, "label": "Title", - "no_copy": 0, + "no_copy": 1, "permlevel": 0, "print_hide": 1, "read_only": 0, @@ -84,7 +84,7 @@ "is_submittable": 0, "issingle": 0, "istable": 0, - "modified": "2015-09-07 15:51:26", + "modified": "2015-09-11 12:20:04.912891", "modified_by": "Administrator", "module": "Desk", "name": "Note", diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index e0c03753db..7d526ceb28 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -33,6 +33,7 @@ class FormMeta(Meta): def load_assets(self): self.add_search_fields() + self.add_linked_document_type() if not self.istable: self.add_linked_with() @@ -49,7 +50,7 @@ class FormMeta(Meta): d[k] = self.get(k) for i, df in enumerate(d.get("fields")): - for k in ("link_doctype", "search_fields", "is_custom_field"): + for k in ("search_fields", "is_custom_field", "linked_document_type"): df[k] = self.get("fields")[i].get(k) return d @@ -120,6 +121,15 @@ class FormMeta(Meta): if search_fields: df.search_fields = map(lambda sf: sf.strip(), search_fields.split(",")) + def add_linked_document_type(self): + for df in self.get("fields", {"fieldtype": "Link"}): + if df.options: + try: + df.linked_document_type = frappe.get_meta(df.options).document_type + except frappe.DoesNotExistError: + # edge case where options="[Select]" + pass + def add_linked_with(self): """add list of doctypes this doctype is 'linked' with. diff --git a/frappe/desk/page/messages/messages.js b/frappe/desk/page/messages/messages.js index 5142cde7ac..5d5a2911dd 100644 --- a/frappe/desk/page/messages/messages.js +++ b/frappe/desk/page/messages/messages.js @@ -41,6 +41,7 @@ frappe.desk.pages.Messages = Class.extend({ }, setup_realtime: function() { + var me = this; frappe.realtime.on('new_message', function(comment) { if(comment.modified_by !== user) { frappe.utils.notify(__("Message from {0}", [comment.comment_by_fullname]), comment.comment); @@ -48,16 +49,20 @@ frappe.desk.pages.Messages = Class.extend({ if (frappe.get_route()[0] === 'messages') { var current_contact = $(cur_page.page).find('[data-contact]').data('contact'); var on_broadcast_page = current_contact === user; - if (current_contact == comment.owner || (on_broadcast_page && comment.broadcast)) { - var $row = $('
'); - frappe.desk.pages.messages.list.data.unshift(comment); - frappe.desk.pages.messages.list.render_row($row, comment); - frappe.desk.pages.messages.list.parent.prepend($row); + if ((current_contact == comment.owner) || (on_broadcast_page && comment.broadcast)) { + me.prepend_comment(comment); } } }); }, + prepend_comment: function(comment) { + var $row = $('
'); + frappe.desk.pages.messages.list.data.unshift(comment); + frappe.desk.pages.messages.list.render_row($row, comment); + frappe.desk.pages.messages.list.$w.prepend($row); + }, + make_sidebar: function() { var me = this; return frappe.call({ @@ -124,7 +129,9 @@ frappe.desk.pages.Messages = Class.extend({ }, callback:function(r,rt) { textarea.val(''); - me.list.run(); + if (!r.exc) { + me.prepend_comment(r.message); + } }, btn: this }); diff --git a/frappe/desk/page/messages/messages.py b/frappe/desk/page/messages/messages.py index 9c050d8390..6fb86fd64a 100644 --- a/frappe/desk/page/messages/messages.py +++ b/frappe/desk/page/messages/messages.py @@ -89,6 +89,8 @@ def post(txt, contact, parenttype=None, notify=False, subject=None): else: _notify(contact, txt, subject) + return d + @frappe.whitelist() def delete(arg=None): frappe.get_doc("Comment", frappe.form_dict['name']).delete() diff --git a/frappe/email/bulk.py b/frappe/email/bulk.py index 3d958fb51f..38aecc35f2 100644 --- a/frappe/email/bulk.py +++ b/frappe/email/bulk.py @@ -10,13 +10,14 @@ from frappe.email.smtp import SMTPServer, get_outgoing_email_account from frappe.email.email_body import get_email, get_formatted_html from frappe.utils.verified_command import get_signed_params, verify_request from html2text import html2text -from frappe.utils import get_url, nowdate, encode, now_datetime, add_days +from frappe.utils import get_url, nowdate, encode, now_datetime, add_days, split_emails class BulkLimitCrossedError(frappe.ValidationError): pass def send(recipients=None, sender=None, subject=None, message=None, reference_doctype=None, reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, - attachments=None, reply_to=None, cc=(), message_id=None, send_after=None): + attachments=None, reply_to=None, cc=(), message_id=None, send_after=None, + expose_recipients=False): """Add email to sending queue (Bulk Email) :param recipients: List of recipients. @@ -39,7 +40,7 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc return if isinstance(recipients, basestring): - recipients = recipients.split(",") + recipients = split_emails(recipients) if isinstance(send_after, int): send_after = add_days(nowdate(), send_after) @@ -66,23 +67,30 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc else: unsubscribed = [] - for email in filter(None, list(set(recipients))): - if email not in unsubscribed: - email_content = formatted - email_text_context = text_content + recipients = [r for r in list(set(recipients)) if r and r not in unsubscribed] - if reference_doctype: - unsubscribe_url = get_unsubcribed_url(reference_doctype, reference_name, email, - unsubscribe_method, unsubscribe_params) + for email in recipients: + email_content = formatted + email_text_context = text_content - # add to queue - email_content = add_unsubscribe_link(email_content, email, reference_doctype, - reference_name, unsubscribe_url, unsubscribe_message) + if reference_doctype: + unsubscribe_link = get_unsubscribe_link( + reference_doctype=reference_doctype, + reference_name=reference_name, + email=email, + recipients=recipients, + expose_recipients=expose_recipients, + unsubscribe_method=unsubscribe_method, + unsubscribe_params=unsubscribe_params, + unsubscribe_message=unsubscribe_message + ) - email_text_context += "\n" + _("This email was sent to {0}. To unsubscribe click on this link: {1}").format(email, unsubscribe_url) + email_content = email_content.replace("", unsubscribe_link.html) + email_text_context += unsubscribe_link.text - add(email, sender, subject, email_content, email_text_context, reference_doctype, - reference_name, attachments, reply_to, cc, message_id, send_after) + # add to queue + add(email, sender, subject, email_content, email_text_context, reference_doctype, + reference_name, attachments, reply_to, cc, message_id, send_after) def add(email, sender, subject, formatted, text_content=None, reference_doctype=None, reference_name=None, attachments=None, reply_to=None, @@ -129,18 +137,41 @@ def check_bulk_limit(recipients): throw(_("Email limit {0} crossed").format(monthly_bulk_mail_limit), BulkLimitCrossedError) -def add_unsubscribe_link(message, email, reference_doctype, reference_name, unsubscribe_url, unsubscribe_message): - unsubscribe_link = """
- {email}. {unsubscribe_message}. - -
""".format(unsubscribe_url = unsubscribe_url, - email= _("This email was sent to {0}").format(email), - unsubscribe_message = unsubscribe_message or _("Unsubscribe from this list")) - - message = message.replace("", unsubscribe_link) - - return message +def get_unsubscribe_link(reference_doctype, reference_name, + email, recipients, expose_recipients, unsubscribe_method, unsubscribe_params, unsubscribe_message): + + unsubscribe_email = recipients if expose_recipients else [email] + unsubscribe_email = _("This email was sent to {0}").format(", ".join(unsubscribe_email)) + + if not unsubscribe_message: + unsubscribe_message = _("Unsubscribe from this list") + + unsubscribe_url = get_unsubcribed_url(reference_doctype, reference_name, email, + unsubscribe_method, unsubscribe_params) + + html = """
+ {email} +

+ {unsubscribe_message} + +

+
""".format( + unsubscribe_url = unsubscribe_url, + email=unsubscribe_email, + unsubscribe_message=unsubscribe_message + ) + + text = "\n{email}\n\n{unsubscribe_message}: {unsubscribe_url}".format( + email=unsubscribe_email, + unsubscribe_message=unsubscribe_message, + unsubscribe_url=unsubscribe_url + ) + + return frappe._dict({ + "html": html, + "text": text + }) def get_unsubcribed_url(reference_doctype, reference_name, email, unsubscribe_method, unsubscribe_params): params = {"email": email.encode("utf-8"), diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 57b00c4fb3..33f1ee0492 100644 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -137,7 +137,7 @@ class EmailAccount(Document): else: frappe.db.commit() - communication.notify(attachments=communication._attachments, except_recipient=True) + communication.notify(attachments=communication._attachments, fetched_from_email_account=True) if exceptions: raise Exception, frappe.as_json(exceptions) @@ -158,6 +158,7 @@ class EmailAccount(Document): "sender_full_name": email.from_real_name, "sender": email.from_email, "recipients": email.mail.get("To"), + "cc": email.mail.get("CC"), "email_account": self.name, "communication_medium": "Email" }) @@ -208,6 +209,9 @@ class EmailAccount(Document): if frappe.db.exists("Communication", in_reply_to): parent = frappe.get_doc("Communication", in_reply_to) + # set in_reply_to of current communication + communication.in_reply_to = in_reply_to + if parent.reference_name: parent = frappe.get_doc(parent.reference_doctype, parent.reference_name) diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 48cfdd87c3..41ce986064 100644 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe.utils.pdf import get_pdf from frappe.email.smtp import get_outgoing_email_account -from frappe.utils import get_url, scrub_urls, strip, expand_relative_urls, cint +from frappe.utils import get_url, scrub_urls, strip, expand_relative_urls, cint, split_emails import email.utils from markdown2 import markdown @@ -42,7 +42,7 @@ class EMail: if isinstance(recipients, basestring): recipients = recipients.replace(';', ',').replace('\n', '') - recipients = recipients.split(',') + recipients = split_emails(recipients) # remove null recipients = filter(None, (strip(r) for r in recipients)) @@ -238,18 +238,18 @@ def get_footer(email_account, footer=None): footer = footer or "" if email_account and email_account.footer: - footer += email_account.footer + footer += '
{0}
'.format(email_account.footer) footer += "" company_address = frappe.db.get_default("email_footer_address") if company_address: - footer += '
{0}
'\ + footer += '
{0}
'\ .format(company_address.replace("\n", "
")) if not cint(frappe.db.get_default("disable_standard_email_footer")): for default_mail_footer in frappe.get_hooks("default_mail_footer"): - footer += default_mail_footer + footer += '
{0}
'.format(default_mail_footer) return footer diff --git a/frappe/geo/doctype/currency/currency.json b/frappe/geo/doctype/currency/currency.json index 4219cb9ba9..b42a53da67 100644 --- a/frappe/geo/doctype/currency/currency.json +++ b/frappe/geo/doctype/currency/currency.json @@ -8,6 +8,7 @@ "description": "**Currency** Master", "docstatus": 0, "doctype": "DocType", + "document_type": "Setup", "fields": [ { "allow_on_submit": 0, @@ -152,7 +153,7 @@ "is_submittable": 0, "issingle": 0, "istable": 0, - "modified": "2015-09-07 15:51:26", + "modified": "2015-09-14 03:17:04.837607", "modified_by": "Administrator", "module": "Geo", "name": "Currency", diff --git a/frappe/hooks.py b/frappe/hooks.py index 13437285c9..0299dff0b2 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -26,7 +26,7 @@ to ERPNext. """ app_icon = "octicon octicon-circuit-board" -app_version = "6.2.0" +app_version = "6.3.0" app_color = "orange" github_link = "https://github.com/frappe/frappe" diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index 6f47b7abb1..bba158f180 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -65,8 +65,11 @@ def set_user_and_static_default_values(doc): def get_user_default_value(df, defaults, user_permissions): # don't set defaults for "User" link field using User Permissions! if df.fieldtype == "Link" and df.options != "User": - # 1 - look in user permissions - if user_permissions_exist(df, user_permissions) and len(user_permissions[df.options])==1: + # 1 - look in user permissions only for document_type==Setup + # We don't want to include permissions of transactions to be used for defaults. + if (frappe.get_meta(df.options).document_type=="Setup" + and user_permissions_exist(df, user_permissions) + and len(user_permissions[df.options])==1): return user_permissions[df.options][0] # 2 - Look in user defaults diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 411d16600c..e4931a6f9c 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -201,56 +201,88 @@ class DatabaseQuery(object): """build conditions from user filters""" if isinstance(filters, dict): filters = [filters] + for f in filters: if isinstance(f, basestring): conditions.append(f) else: - f = self.get_filter_tuple(f) - - tname = ('`tab' + f[0] + '`') - if not tname in self.tables: - self.append_table(tname) - - # prepare in condition - if f[2] in ['in', 'not in']: - opts = f[3] - if not isinstance(opts, (list, tuple)): - opts = f[3].split(",") - opts = [frappe.db.escape(t.strip()) for t in opts] - f[3] = '("{0}")'.format('", "'.join(opts)) - conditions.append('ifnull({tname}.{fname}, "") {operator} {value}'.format( - tname=tname, fname=f[1], operator=f[2], value=f[3])) - else: - df = frappe.get_meta(f[0]).get("fields", {"fieldname": f[1]}) - df = df[0] if df else None - - if df and df.fieldtype=="Date": - value, default_val = '"{0}"'.format(frappe.db.escape(getdate(f[3]).strftime("%Y-%m-%d"))), \ - "'0000-00-00'" - - elif df and df.fieldtype=="Datetime": - value, default_val = '"{0}"'.format(frappe.db.escape(get_datetime(f[3]).strftime("%Y-%m-%d %H:%M:%S.%f"))), \ - "'0000-00-00 00:00:00'" - - elif df and df.fieldtype=="Time": - value, default_val = '"{0}"'.format(frappe.db.escape(get_time(f[3]).strftime("%H:%M:%S.%f"))), \ - "'00:00:00'" - - elif f[2] == "like" or (isinstance(f[3], basestring) and - (not df or df.fieldtype not in ["Float", "Int", "Currency", "Percent", "Check"])): - if f[2] == "like" and isinstance(f[3], basestring): - # because "like" uses backslash (\) for escaping - f[3] = f[3].replace("\\", "\\\\") - - value, default_val = '"{0}"'.format(frappe.db.escape(f[3])), '""' - else: - value, default_val = flt(f[3]), 0 - - conditions.append('ifnull({tname}.{fname}, {default_val}) {operator} {value}'.format( - tname=tname, fname=f[1], default_val=default_val, operator=f[2], - value=value)) - - def get_filter_tuple(self, f): + conditions.append(self.prepare_filter_condition(f)) + + def prepare_filter_condition(self, f): + """Returns a filter condition in the format: + + ifnull(`tabDocType`.`fieldname`, fallback) operator "value" + """ + + f = self.get_filter(f) + + tname = ('`tab' + f.doctype + '`') + if not tname in self.tables: + self.append_table(tname) + + # prepare in condition + if f.operator in ('in', 'not in'): + values = f.value + if not isinstance(values, (list, tuple)): + values = values.split(",") + + values = (frappe.db.escape(v.strip()) for v in values) + values = '("{0}")'.format('", "'.join(values)) + + condition = 'ifnull({tname}.{fname}, "") {operator} {value}'.format( + tname=tname, fname=f.fieldname, operator=f.operator, value=values) + + else: + df = frappe.get_meta(f.doctype).get("fields", {"fieldname": f.fieldname}) + df = df[0] if df else None + + if df and df.fieldtype=="Date": + value = getdate(f.value).strftime("%Y-%m-%d") + fallback = "'0000-00-00'" + + elif df and df.fieldtype=="Datetime": + value = get_datetime(f.value).strftime("%Y-%m-%d %H:%M:%S.%f") + fallback = "'0000-00-00 00:00:00'" + + elif df and df.fieldtype=="Time": + value = get_time(f.value).strftime("%H:%M:%S.%f") + fallback = "'00:00:00'" + + elif f.operator == "like" or (isinstance(f.value, basestring) and + (not df or df.fieldtype not in ["Float", "Int", "Currency", "Percent", "Check"])): + value = f.value + fallback = '""' + + if f.operator == "like" and isinstance(value, basestring): + # because "like" uses backslash (\) for escaping + value = value.replace("\\", "\\\\") + + else: + value = flt(f.value) + fallback = 0 + + # put it inside double quotes + if isinstance(value, basestring): + value = '"{0}"'.format(frappe.db.escape(value)) + + condition = 'ifnull({tname}.{fname}, {fallback}) {operator} {value}'.format( + tname=tname, fname=f.fieldname, fallback=fallback, operator=f.operator, + value=value) + + # replace % with %% to prevent python format string error + return condition.replace("%", "%%") + + def get_filter(self, f): + """Returns a _dict like + + { + "doctype": "DocType", + "fieldname": "fieldname", + "operator": "=", + "value": "value" + } + + """ if isinstance(f, dict): key, value = f.items()[0] f = self.make_filter_tuple(key, value) @@ -262,9 +294,14 @@ class DatabaseQuery(object): f = (self.doctype, f[0], f[1], f[2]) elif len(f) != 4: - frappe.throw("Filter must have 4 values (doctype, fieldname, condition, value): " + str(f)) + frappe.throw("Filter must have 4 values (doctype, fieldname, operator, value): " + str(f)) - return list(f) + return frappe._dict({ + "doctype": f[0], + "fieldname": f[1], + "operator": f[2], + "value": f[3] + }) def build_match_conditions(self, as_condition=True): """add match conditions if applicable""" @@ -313,7 +350,8 @@ class DatabaseQuery(object): conditions = "({conditions}) or ({shared_condition})".format( conditions=conditions, shared_condition=self.get_share_condition()) - return conditions + # replace % with %% to prevent python format string error + return conditions.replace("%", "%%") else: return self.match_filters diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 2cc37e3bee..dbdf145dc4 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -85,7 +85,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa if doc: try: - doc.notify_modified() + doc.notify_update() insert_feed(doc) except ImportError: pass diff --git a/frappe/model/document.py b/frappe/model/document.py index c945c3394b..d90f5d1309 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -167,7 +167,7 @@ class Document(BaseDocument): self.check_permission("create") self._set_defaults() - self._set_docstatus_user_and_timestamp() + self.set_docstatus_user_and_timestamp() self.check_if_latest() self.run_method("before_insert") self.set_new_name() @@ -218,7 +218,7 @@ class Document(BaseDocument): self.check_permission("write", "save") - self._set_docstatus_user_and_timestamp() + self.set_docstatus_user_and_timestamp() self.check_if_latest() self.set_parent_in_children() self.validate_higher_perm_levels() @@ -298,7 +298,7 @@ class Document(BaseDocument): if self.doctype in frappe.db.value_cache: del frappe.db.value_cache[self.doctype] - def _set_docstatus_user_and_timestamp(self): + def set_docstatus_user_and_timestamp(self): self._original_modified = self.modified self.modified = now() self.modified_by = frappe.session.user @@ -599,11 +599,11 @@ class Document(BaseDocument): self.run_method("on_update_after_submit") frappe.cache().hdel("last_modified", self.doctype) - self.notify_modified() + self.notify_update() self.latest = None - def notify_modified(self): + def notify_update(self): """Publish realtime that the current document is modified""" frappe.publish_realtime("doc_update", {"modified": self.modified, "doctype": self.doctype, "name": self.name}, doctype=self.doctype, docname=self.name) diff --git a/frappe/print/doctype/print_format/print_format.json b/frappe/print/doctype/print_format/print_format.json index 61bdc9316d..019091a016 100644 --- a/frappe/print/doctype/print_format/print_format.json +++ b/frappe/print/doctype/print_format/print_format.json @@ -281,6 +281,72 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "css_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "css", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Custom CSS", + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "custom_html_help", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Custom HTML Help", + "no_copy": 0, + "options": "

Custom CSS Help

\n\n

Notes:

\n\n
    \n
  1. All field groups (label + value) are set attributes data-fieldtype and data-fieldname
  2. \n
  3. All values are given class value
  4. \n
  5. All Section Breaks are given class section-break
  6. \n
  7. All Column Breaks are given class column-break
  8. \n
\n\n

Examples

\n\n

1. Left align integers

\n\n
[data-fieldtype=\"Int\"] .value { text-left: left; }
\n\n

1. Add border to sections except the last section

\n\n
.section-break { padding: 30px 0px; border-bottom: 1px solid #eee; }\n.section-break:last-child { padding-bottom: 0px; border-bottom: 0px;  }
\n", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_on_submit": 0, "bold": 0, @@ -381,7 +447,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2015-07-15 08:01:06.284031", + "modified": "2015-09-09 05:46:11.025962", "modified_by": "Administrator", "module": "Print", "name": "Print Format", diff --git a/frappe/print/doctype/print_format/test_print_format.py b/frappe/print/doctype/print_format/test_print_format.py index 936e06402c..aa8c79200d 100644 --- a/frappe/print/doctype/print_format/test_print_format.py +++ b/frappe/print/doctype/print_format/test_print_format.py @@ -12,7 +12,7 @@ class TestPrintFormat(unittest.TestCase): def test_print_user(self, style=None): print_html = frappe.get_print("User", "Administrator", style=style) self.assertTrue("" in print_html) - self.assertTrue(re.findall('
[\s]*Administrator[\s]*
', print_html)) + self.assertTrue(re.findall('
[\s]*Administrator[\s]*
', print_html)) return print_html def test_print_user_standard(self): diff --git a/frappe/public/js/frappe/form/control.js b/frappe/public/js/frappe/form/control.js index 1a133ab00a..011d854fed 100644 --- a/frappe/public/js/frappe/form/control.js +++ b/frappe/public/js/frappe/form/control.js @@ -313,12 +313,6 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ if(this.only_input || this.df.label==this._label) return; - // var icon = frappe.ui.form.fieldtype_icons[this.df.fieldtype]; - // if(this.df.fieldtype==="Link") { - // icon = frappe.boot.doctype_icons[this.df.options]; - // } else if(this.df.link_doctype) { - // icon = frappe.boot.doctype_icons[this.df.link_doctype]; - // } var icon = ""; this.label_span.innerHTML = (icon ? ' ' : "") + __(this.df.label) || " "; diff --git a/frappe/public/js/frappe/form/script_manager.js b/frappe/public/js/frappe/form/script_manager.js index e70adf3112..1395fd81a7 100644 --- a/frappe/public/js/frappe/form/script_manager.js +++ b/frappe/public/js/frappe/form/script_manager.js @@ -78,13 +78,24 @@ frappe.ui.form.ScriptManager = Class.extend({ var tmp = eval(cs); } - // setup add fetch - $.each(this.frm.fields, function(i, field) { - var df = field.df; - if((df.fieldtype==="Read Only" || df.read_only==1) && df.options && df.options.indexOf(".")!=-1) { + function setup_add_fetch(df) { + if(df.fieldname==="size") + console.log(df.fieldname); + if((df.fieldtype==="Read Only" || df.read_only==1) + && df.options && df.options.indexOf(".")!=-1) { var parts = df.options.split("."); me.frm.add_fetch(parts[0], parts[1], df.fieldname); } + } + + // setup add fetch + $.each(this.frm.fields, function(i, field) { + setup_add_fetch(field.df); + if(field.df.fieldtype==="Table") { + $.each(frappe.meta.get_docfields(field.df.options, me.frm.docname), function(i, df) { + setup_add_fetch(df); + }); + } }); // css diff --git a/frappe/public/js/frappe/misc/user.js b/frappe/public/js/frappe/misc/user.js index a4ba377e0c..68813d2f09 100644 --- a/frappe/public/js/frappe/misc/user.js +++ b/frappe/public/js/frappe/misc/user.js @@ -193,6 +193,29 @@ $.extend(frappe.user, { is_report_manager: function() { return frappe.user.has_role(['Administrator', 'System Manager', 'Report Manager']); }, + + get_formatted_email: function(email) { + var fullname = frappe.user.full_name(email); + + if (!fullname) { + return email; + } else { + // to quote or to not + var quote = ''; + + // only if these special characters are found + // why? To make the output same as that in python! + if (fullname.search(/[\[\]\\()<>@,:;".]/) !== -1) { + quote = '"'; + } + + return repl('%(quote)s%(fullname)s%(quote)s <%(email)s>', { + fullname: fullname, + email: email, + quote: quote + }); + } + } }); frappe.session_alive = true; diff --git a/frappe/public/js/frappe/misc/utils.js b/frappe/public/js/frappe/misc/utils.js index e57339350e..db9be6ff9f 100644 --- a/frappe/public/js/frappe/misc/utils.js +++ b/frappe/public/js/frappe/misc/utils.js @@ -43,6 +43,9 @@ frappe.utils = { }); return out.join(newline); }, + escape_html: function(txt) { + return $("
").text(txt || "").html(); + }, is_url: function(txt) { return txt.toLowerCase().substr(0,7)=='http://' || txt.toLowerCase().substr(0,8)=='https://' diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js index 8c29045ede..5e6a56219d 100644 --- a/frappe/public/js/frappe/model/create_new.js +++ b/frappe/public/js/frappe/model/create_new.js @@ -82,8 +82,10 @@ $.extend(frappe.model, { // don't set defaults for "User" link field using User Permissions! if (df.fieldtype==="Link" && df.options!=="User") { - // 1 - look in user permissions - if (has_user_permissions && user_permissions[df.options].length===1) { + // 1 - look in user permissions for document_type=="Setup". + // We don't want to include permissions of transactions to be used for defaults. + if (df.linked_document_type==="Setup" + && has_user_permissions && user_permissions[df.options].length===1) { return user_permissions[df.options][0]; } diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 86105afc39..ac4d72f3e0 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -154,10 +154,11 @@ $.extend(frappe.model, { }, new_comment: function(comment) { - if (frappe.model.docinfo[comment.comment_doctype] - && frappe.model.docinfo[comment.comment_doctype][comment.comment_docname]) { - var comments = frappe.model.docinfo[comment.comment_doctype][comment.comment_docname].comments; + var reference_doctype = comment.comment_doctype || comment.reference_doctype; + var reference_name = comment.comment_docname || comment.reference_name; + if (frappe.model.docinfo[reference_doctype] && frappe.model.docinfo[reference_doctype][reference_name]) { + var comments = frappe.model.docinfo[reference_doctype][reference_name].comments; var comment_exists = false; for (var i=0, l=comments.length; i') .appendTo(msg_dialog.body); + msg_dialog.clear = function() { + msg_dialog.msg_area.empty(); + } } if(msg.search(/
|

|

  • /)==-1) @@ -175,6 +178,13 @@ frappe.show_progress = function(title, count, total) { dialog.progress_bar.css({"width": cint(flt(count) * 100 / total) + "%" }); } +frappe.hide_progress = function() { + if(frappe.cur_progress) { + frappe.cur_progress.hide(); + frappe.cur_progress = null; + } +} + // Floating Message function show_alert(txt, seconds) { if(!$('#dialog-container').length) { diff --git a/frappe/public/js/frappe/ui/star.js b/frappe/public/js/frappe/ui/star.js index 01a06b6a4a..c07516bf70 100644 --- a/frappe/public/js/frappe/ui/star.js +++ b/frappe/public/js/frappe/ui/star.js @@ -2,12 +2,17 @@ // MIT License. See license.txt frappe.ui.is_starred = function(doc) { + var starred = frappe.ui.get_starred_by(doc); + return starred.indexOf(user)===-1 ? false : true; +} + +frappe.ui.get_starred_by = function(doc) { var starred = doc._starred_by; if(starred) { starred = JSON.parse(starred); - return starred.indexOf(user)===-1 ? false : true; } - return false; + + return starred || []; } frappe.ui.toggle_star = function($btn, doctype, name) { diff --git a/frappe/public/js/frappe/upload.js b/frappe/public/js/frappe/upload.js index 4d878cc59a..2f34ee113d 100644 --- a/frappe/public/js/frappe/upload.js +++ b/frappe/public/js/frappe/upload.js @@ -81,7 +81,7 @@ frappe.upload = { if(opts.start) { opts.start(); } - return frappe.call({ + ajax_args = { "method": "uploadfile", args: args, callback: function(r) { @@ -96,23 +96,18 @@ frappe.upload = { opts.callback(attachment, r); $(document).trigger("upload_complete", attachment); }, - progress: function(data) { - if(opts.progress) { - opts.progress(data); - } - }, - always: function(data) { - if(opts.always) { - opts.always(data); - } - }, error: function(r) { // if no onerror, assume callback will handle errors opts.onerror ? opts.onerror(r) : opts.callback(null, null, r); return; - }, - btn: opts.btn + } + } + + // copy handlers etc from opts + $.each(['queued', 'running', "progress", "always", "btn"], function(i, key) { + if(opts[key]) ajax_args[key] = opts[key]; }); + return frappe.call(ajax_args); } } diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 6d0246456b..31eca633c4 100644 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -14,41 +14,7 @@ frappe.views.CommunicationComposer = Class.extend({ this.dialog = new frappe.ui.Dialog({ title: __("Add Reply") + ": " + (this.subject || ""), no_submit_on_enter: true, - fields: [ - {label:__("To"), fieldtype:"Data", reqd: 1, fieldname:"recipients"}, - - {fieldtype: "Section Break"}, - {fieldtype: "Column Break"}, - {label:__("Subject"), fieldtype:"Data", reqd: 1, - fieldname:"subject"}, - {fieldtype: "Column Break"}, - {label:__("Standard Reply"), fieldtype:"Link", options:"Standard Reply", - fieldname:"standard_reply"}, - - {fieldtype: "Section Break"}, - {label:__("Message"), fieldtype:"Text Editor", reqd: 1, - fieldname:"content"}, - - {fieldtype: "Section Break"}, - {fieldtype: "Column Break"}, - {label:__("Send As Email"), fieldtype:"Check", - fieldname:"send_email"}, - {label:__("Send me a copy"), fieldtype:"Check", - fieldname:"send_me_a_copy"}, - {label:__("Communication Medium"), fieldtype:"Select", - options: ["Phone", "Chat", "Email", "SMS", "Visit", "Other"], - fieldname:"communication_medium"}, - {label:__("Sent or Received"), fieldtype:"Select", - options: ["Received", "Sent"], - fieldname:"sent_or_received"}, - {label:__("Attach Document Print"), fieldtype:"Check", - fieldname:"attach_document_print"}, - {label:__("Select Print Format"), fieldtype:"Select", - fieldname:"select_print_format"}, - {fieldtype: "Column Break"}, - {label:__("Select Attachments"), fieldtype:"HTML", - fieldname:"select_attachments"} - ], + fields: this.get_fields(), primary_action_label: "Send", primary_action: function() { me.send_action(); @@ -79,6 +45,100 @@ frappe.views.CommunicationComposer = Class.extend({ this.dialog.show(); }, + + get_fields: function() { + var cc_fields = this.get_cc_fields(); + + var fields_before_cc = [ + {fieldtype: "Section Break"}, + {label:__("To"), fieldtype:"Data", reqd: 1, fieldname:"recipients"}, + {fieldtype: "Section Break", collapsible: 1, label: "CC & Standard Reply"}, + {label:__("CC"), fieldtype:"Data", fieldname:"cc"}, + ]; + + var fields_after_cc = [ + {label:__("Standard Reply"), fieldtype:"Link", options:"Standard Reply", + fieldname:"standard_reply"}, + {fieldtype: "Section Break"}, + {label:__("Subject"), fieldtype:"Data", reqd: 1, + fieldname:"subject"}, + {fieldtype: "Section Break"}, + {label:__("Message"), fieldtype:"Text Editor", reqd: 1, + fieldname:"content"}, + {fieldtype: "Section Break"}, + {fieldtype: "Column Break"}, + {label:__("Send As Email"), fieldtype:"Check", + fieldname:"send_email"}, + {label:__("Send me a copy"), fieldtype:"Check", + fieldname:"send_me_a_copy"}, + {label:__("Communication Medium"), fieldtype:"Select", + options: ["Phone", "Chat", "Email", "SMS", "Visit", "Other"], + fieldname:"communication_medium"}, + {label:__("Sent or Received"), fieldtype:"Select", + options: ["Received", "Sent"], + fieldname:"sent_or_received"}, + {label:__("Attach Document Print"), fieldtype:"Check", + fieldname:"attach_document_print"}, + {label:__("Select Print Format"), fieldtype:"Select", + fieldname:"select_print_format"}, + {fieldtype: "Column Break"}, + {label:__("Select Attachments"), fieldtype:"HTML", + fieldname:"select_attachments"} + ]; + + return fields_before_cc.concat(cc_fields).concat(fields_after_cc); + }, + + get_cc_fields: function() { + var cc = [ [this.frm.doc.owner, 1] ]; + + var starred_by = frappe.ui.get_starred_by(this.frm.doc); + if (starred_by) { + for ( var i=0, l=starred_by.length; i -
    +
    @@ -38,7 +38,9 @@
    + {% if background_image %} + {% endif %} diff --git a/frappe/templates/pages/print.html b/frappe/templates/pages/print.html index 0e69a533f4..6136426593 100644 --- a/frappe/templates/pages/print.html +++ b/frappe/templates/pages/print.html @@ -5,7 +5,10 @@ {{ title }} - + + diff --git a/frappe/templates/pages/print.py b/frappe/templates/pages/print.py index 46384d3496..9338adabd1 100644 --- a/frappe/templates/pages/print.py +++ b/frappe/templates/pages/print.py @@ -18,9 +18,6 @@ base_template_path = "templates/pages/print.html" standard_format = "templates/print_formats/standard.html" def get_context(context): - if not frappe.form_dict.format: - frappe.form_dict.format = standard_format - if not frappe.form_dict.doctype or not frappe.form_dict.name: return { "body": """

    Error

    @@ -31,14 +28,30 @@ def get_context(context): doc = frappe.get_doc(frappe.form_dict.doctype, frappe.form_dict.name) meta = frappe.get_meta(doc.doctype) + print_format = get_print_format_doc() + return { - "body": get_html(doc, print_format = frappe.form_dict.format, - meta=meta, trigger_print = frappe.form_dict.trigger_print, no_letterhead=frappe.form_dict.no_letterhead), - "css": get_print_style(frappe.form_dict.style, frappe.form_dict.format), + "body": get_html(doc, print_format = print_format, + meta=meta, trigger_print = frappe.form_dict.trigger_print, + no_letterhead=frappe.form_dict.no_letterhead), + "css": get_print_style(frappe.form_dict.style, print_format), "comment": frappe.session.user, "title": doc.get(meta.title_field) if meta.title_field else doc.name } +def get_print_format_doc(print_format_name=None): + """Returns print format document""" + if not print_format_name: + print_format_name = frappe.form_dict.format or "Standard" + + if not print_format_name: + print_format_name = "Standard" + + if print_format_name == "Standard": + return None + else: + return frappe.get_doc("Print Format", print_format_name) + def get_html(doc, name=None, print_format=None, meta=None, no_letterhead=None, trigger_print=False): @@ -71,11 +84,7 @@ def get_html(doc, name=None, print_format=None, meta=None, format_data, format_data_map = [], {} # determine template - if print_format in ("Standard", standard_format): - template = "standard" - else: - print_format = frappe.get_doc("Print Format", print_format) - + if print_format: if print_format.standard=="Yes" or print_format.custom_format: template = jenv.from_string(get_print_format(doc.doctype, print_format)) @@ -97,8 +106,12 @@ def get_html(doc, name=None, print_format=None, meta=None, # fallback template = "standard" + else: + template = "standard" + + if template == "standard": - template = jenv.get_template("templates/print_formats/standard.html") + template = jenv.get_template(standard_format) args = { "doc": doc, @@ -119,6 +132,7 @@ def get_html(doc, name=None, print_format=None, meta=None, @frappe.whitelist() def get_html_and_style(doc, name=None, print_format=None, meta=None, no_letterhead=None, trigger_print=False): + print_format = get_print_format_doc(print_format) return { "html": get_html(doc, name=name, print_format=print_format, meta=meta, no_letterhead=no_letterhead, trigger_print=trigger_print), @@ -200,6 +214,10 @@ def make_layout(doc, meta, format_data=None): df.print_hide = 0 if df.fieldtype=="Section Break" or page==[]: + if len(page) > 1 and not any(page[-1]): + # truncate prev section if empty + del page[-1] + page.append([]) if df.fieldtype=="Column Break" or (page[-1]==[] and df.fieldtype!="Section Break"): @@ -232,9 +250,6 @@ def make_layout(doc, meta, format_data=None): df.end = None page[-1][-1].append(df) - # filter empty sections - layout = [filter(lambda s: any(filter(lambda c: any(c), s)), pg) for pg in layout] - return layout def is_visible(df, doc): @@ -256,6 +271,9 @@ def has_value(df, doc): elif isinstance(value, basestring) and not strip_html(value).strip(): return False + elif isinstance(value, list) and not len(value): + return False + return True def get_print_style(style=None, print_format=None, for_legacy=False): @@ -284,6 +302,9 @@ def get_print_style(style=None, print_format=None, for_legacy=False): # prepend css with at_import css = at_import + css + if print_format and print_format.css: + css += "\n\n" + print_format.css + return css def get_font(print_settings, print_format=None, for_legacy=False): @@ -292,8 +313,7 @@ def get_font(print_settings, print_format=None, for_legacy=False): return default font = None - if print_format and print_format not in ("Standard", standard_format): - print_format = frappe.get_doc("Print Format", print_format) + if print_format: if print_format.font and print_format.font!="Default": font = '{0}, sans-serif'.format(print_format.font) diff --git a/frappe/templates/pages/sitemap.py b/frappe/templates/pages/sitemap.py index d797cdc609..1216e784fd 100644 --- a/frappe/templates/pages/sitemap.py +++ b/frappe/templates/pages/sitemap.py @@ -25,7 +25,7 @@ def get_context(context): for route, data in get_generator_routes().iteritems(): links.append({ - "loc": urllib.basejoin(host, urllib.quote(route.encode("utf-8"))), + "loc": urllib.basejoin(host, urllib.quote((route or "").encode("utf-8"))), "lastmod": get_datetime(data.get("modified")).strftime("%Y-%m-%d") }) diff --git a/frappe/templates/print_formats/standard.html b/frappe/templates/print_formats/standard.html index fc0b121692..c2f5b6ea83 100644 --- a/frappe/templates/print_formats/standard.html +++ b/frappe/templates/print_formats/standard.html @@ -5,9 +5,9 @@
    {{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead) }} {% for section in page %} -
    +
    {% for column in section %} -
    +
    {% for df in column %} {{ render_field(df, doc) }} {% endfor %} diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index 9951252a25..4cea95bbd6 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -20,13 +20,13 @@ {%- if data -%} {%- set visible_columns = get_visible_columns(doc.get(df.fieldname), table_meta, df) -%} -
    +
    - + {% for tdf in visible_columns %} - {% endfor %} @@ -34,10 +34,10 @@ {% for d in data %} - + {% for tdf in visible_columns %} - + {% endfor %} {% endfor %} @@ -48,15 +48,19 @@ {%- endif -%} {%- endmacro -%} +{% macro fieldmeta(df) -%} +data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}" +{%- endmacro %} + {%- macro render_field_with_label(df, doc) -%} -
    +
    {% if df.fieldtype not in ("Image","HTML") and doc.get(df.fieldname) != None %} {% endif %}
    -
    +
    {% if doc.get(df.fieldname) != None -%} {{ print_value(df, doc) }}{% endif %}
    @@ -65,10 +69,10 @@ {%- macro render_text_field(df, doc) -%} {%- if doc.get(df.fieldname) != None -%} -
    +
    {%- if df.fieldtype in ("Text", "Code") %}{%- endif %} {%- if df.fieldtype=="Code" %} -
    {{ doc.get(df.fieldname) }}
    +
    {{ doc.get(df.fieldname) }}
    {% else -%} {{ doc.get_formatted(df.fieldname, parent_doc or doc) }} {% endif -%} @@ -98,7 +102,7 @@ {%- endmacro %} {% macro get_align_class(fieldtype) %} - {%- if fieldtype in ("Int", "Check", "Float", "Currency") -%}{{ "text-right" }} + {%- if fieldtype in ("Int", "Float", "Currency") -%}{{ "text-right" }} {%- else -%}{{ "" }} {%- endif -%} {% endmacro %} diff --git a/frappe/tests/test_async.py b/frappe/tests/test_async.py index e1f5c3e673..da22c0df03 100644 --- a/frappe/tests/test_async.py +++ b/frappe/tests/test_async.py @@ -11,7 +11,7 @@ from frappe.tasks import run_async_task class TestAsync(unittest.TestCase): def test_response(self): - result = run_async_task.delay(frappe.local.site, 'Administrator', 'async_ping', - frappe._dict()) + result = run_async_task.delay(site=frappe.local.site, user='Administrator', cmd='async_ping', + form_dict=frappe._dict()) result = result.get() self.assertEquals(result.get("message"), "pong") diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index 1805aab9fb..007f8690b2 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -46,7 +46,7 @@ DocType: Feed,Color,Farbe DocType: Workflow State,indent-right,Einzug rechts sites/assets/js/desk.min.js +846,Web Link,Weblink apps/frappe/frappe/core/page/data_import_tool/data_import_main.html +33,"Recommended bulk editing records via import, or understanding the import format.",Empfohlene Groß Bearbeiten von Datensätzen über die Einfuhr oder das Verständnis der Importformat. -apps/frappe/frappe/core/page/permission_manager/permission_manager_help.html +36,"Apart from System Manager, roles with Set User Permissions right can set permissions for other users for that Document Type.",Neben dem System-Manager-Rollen mit Set Benutzerberechtigungen können mit der rechten Berechtigungen für andere Benutzer für diesen Dokumenttyp eingestellt. +apps/frappe/frappe/core/page/permission_manager/permission_manager_help.html +36,"Apart from System Manager, roles with Set User Permissions right can set permissions for other users for that Document Type.",Neben dem System-Manager können Rollen mit der Erlaubnis Benutzer anzulegen Berechtigungen für andere Nutzer für diesen Dokumententyp setzen. DocType: Company History,Company History,Unternehmensgeschichte DocType: Workflow State,volume-up,Lautstärke erhöhen apps/frappe/frappe/core/page/data_import_tool/importer.py +43,Only allowed {0} rows in one import,Nur darf {0} Zeilen in einer Import- @@ -109,7 +109,7 @@ DocType: System Settings,Run scheduled jobs only if checked,"Führen Sie geplant DocType: User,"Enter default value fields (keys) and values. If you add multiple values for a field, the first one will be picked. These defaults are also used to set ""match"" permission rules. To see list of fields, go to ""Customize Form"".","Geben Sie Standardwert Felder (Schlüssel) und Werte. Wenn Sie mehrere Werte für ein Feld hinzufügen, wird der erste, abgeholt werden. Diese Standardwerte werden auch zur "match" Berechtigungsregeln festgelegt. Zur Liste der Felder zu sehen, gehen Sie auf "Customize Form"." DocType: Customize Form Field,"Print Width of the field, if the field is a column in a table","Druckbreite des Feldes, wenn das Feld eine Spalte aus einer Tabelle ist" DocType: Workflow State,headphones,Kopfhörer -DocType: Bulk Email,Bulk Email records.,Spam-Aufzeichnungen +DocType: Bulk Email,Bulk Email records.,Massen-Email-Aufzeichnungen DocType: Email Account,e.g. replies@yourcomany.com. All replies will come to this inbox.,zB replies@yourcomany.com. Alle Antworten werden zu diesem Posteingang zu kommen. apps/frappe/frappe/templates/includes/login/login.js +32,Valid email and name required,Gültige E-Mail und Name erforderlich DocType: DocType,Hide Heading,Überschrift ausblenden @@ -146,7 +146,7 @@ DocType: Website Settings,Add a banner to the site. (small banners are usually g DocType: Website Settings,Set Banner from Image,Banner von Bild einrichten DocType: Print Format,Verdana,Verdana apps/frappe/frappe/core/doctype/user/user.py +203,User {0} cannot be deleted,Benutzer {0} kann nicht gelöscht werden -sites/assets/js/desk.min.js +265,Another transaction is blocking this one. Please try again in a few seconds.,Eine weitere Transaktion blockiert diese. Bitte versuchen Sie es in ein paar Sekunden. +sites/assets/js/desk.min.js +265,Another transaction is blocking this one. Please try again in a few seconds.,Eine andere Transaktion blockiert die aktuelle. Bitte versuchen Sie es in ein paar Sekunden noch einmal. DocType: Property Setter,Field Name,Feldname sites/assets/js/desk.min.js +771,or,oder sites/assets/js/desk.min.js +925,module name...,Modulnamen ... @@ -210,14 +210,14 @@ sites/assets/js/desk.min.js +921,"document type..., e.g. customer","Dokumenttyp DocType: Country,Country Name,Ländername DocType: About Us Team Member,About Us Team Member,"""Über uns"" Teammitglieder" apps/frappe/frappe/core/page/permission_manager/permission_manager_help.html +5,"Permissions are set on Roles and Document Types (called DocTypes) by setting rights like Read, Write, Create, Delete, Submit, Cancel, Amend, Report, Import, Export, Print, Email and Set User Permissions.","Berechtigungen werden auf Rollen und Dokumenttypen (genannt DocTypes ) , indem Sie Rechte wie Lesen, Schreiben, Erstellen, Löschen , Senden , Abbrechen Amend , Bericht, Import, Export, Drucken , E-Mail und Set- Benutzerberechtigungen festlegen." -apps/frappe/frappe/core/page/user_permissions/user_permissions.js +19,"Apart from Role based Permission Rules, you can apply User Permissions based on DocTypes.","Neben Rollenbasierte Berechtigungsregeln , können Sie Benutzerberechtigungen auf der Grundlage DocTypes gelten ." +apps/frappe/frappe/core/page/user_permissions/user_permissions.js +19,"Apart from Role based Permission Rules, you can apply User Permissions based on DocTypes.",Neben rollenbasierten Berechtigungsregeln können Sie Benutzerberechtigungen auf der Grundlage von DocTypes einsetzen. apps/frappe/frappe/core/page/user_permissions/user_permissions.js +23,"These permissions will apply for all transactions where the permitted record is linked. For example, if Company C is added to User Permissions of user X, user X will only be able to see transactions that has company C as a linked value.","Diese Berechtigungen werden für alle Transaktionen, bei denen die zulässige Datensatz verknüpft anzuwenden." DocType: Property Setter,ID (name) of the entity whose property is to be set,"ID (Name) der Einheit, deren Eigenschaft festgelegt werden muss" DocType: Website Settings,Website Theme Image Link,Website Theme Bild Link DocType: Website Settings,Sidebar Items,Sidebar Artikel DocType: Workflow State,exclamation-sign,Ausrufezeichen DocType: Website Theme,Hide Sidebar,Sidebar ausblenden -sites/assets/js/list.min.js +100,Gantt,Gantt +sites/assets/js/list.min.js +100,Gantt,Gantt-Diagramm DocType: About Us Settings,Introduce your company to the website visitor.,Präsentieren Sie Ihr Unternehmen dem Besucher der Website. apps/frappe/frappe/print/doctype/print_format/print_format.js +14,Please duplicate this to make changes,Bitte vervielfältigen Sie diese Änderungen vornehmen apps/frappe/frappe/utils/pdf.py +35,PDF generation failed because of broken image links,"PDF-Generierung ist fehlgeschlagen, weil der gebrochenen Bild Links" @@ -250,10 +250,10 @@ apps/frappe/frappe/core/page/data_import_tool/data_import_tool.py +15,Parent Tab apps/frappe/frappe/website/doctype/website_settings/website_settings.py +38,{0} in row {1} cannot have both URL and child items,{0} in Zeile {1} kann nicht sowohl die URL als auch Unterpunkte haben apps/frappe/frappe/utils/nestedset.py +194,Root {0} cannot be deleted,Wurzel {0} kann nicht gelöscht werden apps/frappe/frappe/website/doctype/blog_post/blog_post.py +162,No comments yet,Noch keine Kommentare -apps/frappe/frappe/print/page/print_format_builder/print_format_builder.js +120,Both DocType and Name required,Sowohl DocType und Name erforderlich +apps/frappe/frappe/print/page/print_format_builder/print_format_builder.js +120,Both DocType and Name required,DocType und Name sind beide erforderlich apps/frappe/frappe/model/document.py +424,Cannot change docstatus from 1 to 0,DocStatus kann nicht geändert werden 1-0 DocType: Workflow Transition,Defines actions on states and the next step and allowed roles.,"Definiert Aktionen bei bestimmten Status, den nächsten Schritt und erlaubte Rollen." -apps/frappe/frappe/core/page/permission_manager/permission_manager_help.html +11,"As a best practice, do not assign the same set of permission rule to different Roles. Instead, set multiple Roles to the same User.","Als bewährte Methode , nicht den gleichen Satz von Berechtigungs Vorschrift auf unterschiedliche Rollen zuzuweisen. Stattdessen legen mehrere Rollen auf den gleichen Benutzer ." +apps/frappe/frappe/core/page/permission_manager/permission_manager_help.html +11,"As a best practice, do not assign the same set of permission rule to different Roles. Instead, set multiple Roles to the same User.","Ein bewährtes Verfahren ist es, den gleichen Satz von Berechtigungen nicht unterschiedlichen Rollen zuzuweisen. Legen Sie stattdessen mehrere Rollen für den gleichen Benutzer an." DocType: Web Form,Message to be displayed on successful completion,Nachricht an den erfolgreichen Abschluss angezeigt DocType: Website Settings,Footer Items,Inhalte der Fußzeile sites/assets/js/desk.min.js +407,Menu,Menü @@ -296,7 +296,7 @@ sites/assets/js/editor.min.js +103,Bold (Ctrl/Cmd+B),Fett (Strg / Cmd + B) apps/frappe/frappe/core/page/user_permissions/user_permissions.js +140,Upload and Sync,Upload und Sync sites/assets/js/form.min.js +280,Shared with {0},Mit Gemeinschaftsbad {0} DocType: DocType,Single Types have only one record no tables associated. Values are stored in tabSingles,Einzelne Arten haben nur ein Datensatz keine Tabellen verbunden. Die Werte werden in tabSingles gespeichert -DocType: Bulk Email,Bulk Email,Massenmail +DocType: Bulk Email,Bulk Email,Massen-Email DocType: Workflow,Rules defining transition of state in the workflow.,"Regeln, die den Übergang des Status in den Workflow definieren." DocType: DocField,Index,Index apps/frappe/frappe/custom/doctype/custom_field/custom_field.js +55,Option 1,Option 1 @@ -356,7 +356,7 @@ DocType: Workflow State,circle-arrow-right,Kreis-Pfeil-nach-rechts DocType: Scheduler Log,Method,Methode DocType: Report,Script Report,Skriptbericht DocType: About Us Settings,Company Introduction,Vorstellung des Unternehmens -DocType: DocPerm,Apply User Permissions,Anwenden von Benutzerberechtigungen +DocType: DocPerm,Apply User Permissions,Benutzerberechtigungen anwenden DocType: User,Modules HTML,Module HTML sites/assets/js/desk.min.js +484,Missing Values Required,Angaben zu erforderlichen Werten fehlen sites/assets/js/list.min.js +107,{0} is not set,{0} ist nicht gesetzt @@ -412,7 +412,7 @@ DocType: DocType,Hide Toolbar,Symbolleiste ausblenden DocType: Email Account,SMTP Settings for outgoing emails,SMTP-Einstellungen für ausgehende E-Mails apps/frappe/frappe/core/page/data_import_tool/data_import_tool.js +125,Import Failed,Import fehlgeschlagen apps/frappe/frappe/templates/emails/password_update.html +3,Your password has been updated. Here is your new password,Ihr Passwort wurde aktualisiert. Hier ist Ihr neues Passwort -DocType: Email Account,Auto Reply Message,Automatische Antwortnachricht +DocType: Email Account,Auto Reply Message,Automatische Rückantwort DocType: Email Alert,Condition,Zustand sites/assets/js/desk.min.js +924,Open a module or tool,Öffnen Sie ein Modul oder Werkzeug DocType: Module Def,App Name,App-Name @@ -473,7 +473,7 @@ DocType: System Settings,yyyy-mm-dd,JJJJ-MM-TT apps/frappe/frappe/email/doctype/email_account/email_account.py +37,Login Id is required,Login-ID wird benötigt DocType: Website Slideshow,Website Slideshow,Webseite Diaschau DocType: Website Settings,"Link that is the website home page. Standard Links (index, login, products, blog, about, contact)","Link, ist die Homepage der Website . Standard- Verbindungen (Index , Login, Artikel , Blog , über, Kontakt)" -sites/assets/js/editor.min.js +105,Bullet list,Bullet list +sites/assets/js/editor.min.js +105,Bullet list,Liste der Gliederungspunkte DocType: Website Settings,Banner Image,Banner Bild DocType: Custom Field,Custom Field,Benutzerdefiniertes Feld apps/frappe/frappe/email/doctype/email_alert/email_alert.py +13,Please specify which date field must be checked,Bitte geben Sie die Datumsfeld überprüft werden muss @@ -587,7 +587,7 @@ DocType: Website Theme,Link to your Bootstrap theme,Link zu Ihrer Bootstrap Them sites/assets/js/desk.min.js +593,Edit as {0},Anpassen als {0} apps/frappe/frappe/core/page/user_permissions/user_permissions.js +194,No User Restrictions found.,Keine Benutzer Einschränkungen gefunden. apps/frappe/frappe/email/doctype/email_account/email_account_list.js +10,Default Inbox,Standard-Posteingang -sites/assets/js/desk.min.js +607,Make a new,erstellen Sie einen neuen Eintrag für: +sites/assets/js/desk.min.js +607,Make a new,Neu anlegen DocType: Print Settings,PDF Page Size,PDF-Seitengröße sites/assets/js/desk.min.js +947,About,Information apps/frappe/frappe/core/page/data_import_tool/exporter.py +66,"For updating, you can update only selective columns.",Für die Aktualisierung können Sie nur selektive Spalten aktualisieren. @@ -598,7 +598,7 @@ DocType: Workflow State,list-alt,Liste-Alt apps/frappe/frappe/templates/pages/update-password.html +63,Password Updated,Aktualisiert Passwort apps/frappe/frappe/core/page/modules_setup/modules_setup.js +18,"Select modules to be shown (based on permission). If hidden, they will be hidden for all users.","Wählen Sie Module gezeigt werden (basierend auf Genehmigung) . Wenn ausgeblendet , werden sie für alle Benutzer ausgeblendet werden." apps/frappe/frappe/core/page/user_permissions/user_permissions.js +149,Permissions Updated,Berechtigungen aktualisiert -apps/frappe/frappe/email/doctype/email_account/email_account.py +45,Append To is mandatory for incoming mails,Anhängen ist für eingehende E-Mails +apps/frappe/frappe/email/doctype/email_account/email_account.py +45,Append To is mandatory for incoming mails,Anhängen an ist für eingehende E-Mails zwingend apps/frappe/frappe/core/doctype/doctype/doctype.py +239,Options requried for Link or Table type field {0} in row {1},Optionen für Link -oder Tabellenfeldrequried {0} in Zeile {1} DocType: Report,Query Report,Abfragebericht DocType: Communication,On,Auf @@ -649,7 +649,7 @@ apps/frappe/frappe/core/page/permission_manager/permission_manager_help.html +14 apps/frappe/frappe/desk/doctype/todo/todo_list.js +7,To Do,Aufgaben sites/assets/js/editor.min.js +94,Paragraph,Absatz apps/frappe/frappe/core/page/user_permissions/user_permissions.js +133,Any existing permission will be deleted / overwritten.,Alle vorhandenen Genehmigung wird gelöscht / überschrieben werden. -apps/frappe/frappe/desk/doctype/todo/todo.py +17,Assigned to {0}: {1},Zugeordnet {0}: {1} +apps/frappe/frappe/desk/doctype/todo/todo.py +17,Assigned to {0}: {1},Zugewiesen zu {0}: {1} DocType: DocField,Percent,Prozent DocType: Workflow State,book,Buch DocType: Website Settings,Landing Page,Landingpage @@ -776,7 +776,7 @@ apps/frappe/frappe/core/doctype/user/user.py +81,New password emailed,Neues Pass apps/frappe/frappe/auth.py +209,Login not allowed at this time,Anmelden zu diesem Zeitpunkt nicht erlaubt DocType: DocType,Permissions Settings,Berechtigungseinstellungen sites/assets/js/desk.min.js +931,{0} List,{0} Liste -apps/frappe/frappe/desk/form/assign_to.py +39,Already in user's To Do list,Bereits im Benutzer- To -Do-Liste +apps/frappe/frappe/desk/form/assign_to.py +39,Already in user's To Do list,Bereits in der ToDo-Liste des Benutzers DocType: Email Account,Enable Outgoing,Abgehende aktivieren DocType: System Settings,Email Footer Address,Email Address Footer DocType: DocField,Text,Text @@ -858,8 +858,8 @@ DocType: Event,Participants,Teilnehmer DocType: Bulk Email,Reference DocName,Referenz-Dokumentenname DocType: Web Form,Success Message,Erfolgsmeldung DocType: DocType,User Cannot Search,Benutzer kann nicht suchen -DocType: DocPerm,Apply this rule if the User is the Owner,"Regel anwenden, wenn der Nutzer den Besitzer" -apps/frappe/frappe/desk/page/activity/activity.js +47,Build Report,Bauen Bericht +DocType: DocPerm,Apply this rule if the User is the Owner,"Diese Regel anwenden, wenn der Nutzer gleich dem Besitzer ist" +apps/frappe/frappe/desk/page/activity/activity.js +47,Build Report,Bericht erstellen apps/frappe/frappe/model/rename_doc.py +91,"{0} {1} does not exist, select a new target to merge",{0} {1} existiert nicht. Bitte ein anderes Ziel wählen apps/frappe/frappe/core/page/user_permissions/user_permissions.py +66,Cannot set permission for DocType: {0} and Name: {1},Kann Erlaubnis nicht gesetzt für DocType : {0} und Name: {1} DocType: Comment,Comment Doctype,Dokumententyp Kommentar @@ -892,7 +892,7 @@ DocType: Event,Every Week,Wöchentlich DocType: Custom Field,Is Mandatory Field,Ist Pflichtfeld DocType: User,Website User,Webseitenbenutzer DocType: Website Script,Script to attach to all web pages.,"Skript, das allen Webseiten hinzugefügt wird." -DocType: Web Form,Allow Multiple,Allow Multiple +DocType: Web Form,Allow Multiple,mehrere zulassen sites/assets/js/form.min.js +291,Assign,Zuweisen apps/frappe/frappe/config/setup.py +93,Import / Export Data from .csv files.,Import / Export von Daten aus .CSV -Dateien. DocType: Workflow State,Icon will appear on the button,Symbol wird auf der Schaltfläche angezeigt @@ -912,7 +912,7 @@ apps/frappe/frappe/model/delete_doc.py +153,Cannot delete or cancel because {0} DocType: Website Settings,Twitter Share,Twitter-Freigabe DocType: Workflow State,Workflow State,Workflow-Status apps/frappe/frappe/templates/pages/me.html +1,My Account,Ihr Konto -DocType: ToDo,Allocated To,Zugeordnet +DocType: ToDo,Allocated To,Zugewiesen zu apps/frappe/frappe/templates/emails/password_reset.html +4,Please click on the following link to set your new password,Bitte klicken Sie auf den folgenden Link um ein neues Passwort festzulegen DocType: Email Alert,Days After,Tage nach DocType: Contact Us Settings,Settings for Contact Us Page,Einstellungen für die Kontaktseite @@ -933,7 +933,7 @@ DocType: DocType,In Dialog,Im Dialog apps/frappe/frappe/custom/doctype/customize_form/customize_form.js +142,Help,Hilfe DocType: User,Login Before,Anmelden vor DocType: Web Page,Insert Style,Stil einfügen -apps/frappe/frappe/desk/page/applications/applications.js +94,Application Installer,Application Installer +apps/frappe/frappe/desk/page/applications/applications.js +94,Application Installer,Einrichtungsprogramm für Anwendung apps/frappe/frappe/core/page/user_permissions/user_permissions.js +246,Is,Ist DocType: Workflow State,info-sign,Info-Zeichen DocType: Currency,"How should this currency be formatted? If not set, will use system defaults","Wie soll diese Währung formatiert werden? Wenn nicht festgelegt, werden die Systemstands verwendet" @@ -967,7 +967,7 @@ apps/frappe/frappe/desk/moduleview.py +57,Module Not Found,Modul nicht gefunden DocType: User,Location,Lage ,Permitted Documents For User,Zulässige Dokumente Nach Benutzer apps/frappe/frappe/core/doctype/docshare/docshare.py +40,"You need to have ""Share"" permission","Sie müssen ""Teilen"" Berechtigung haben" -sites/assets/js/form.min.js +213,Bulk Edit {0},Massenbearbeitung {0} +sites/assets/js/form.min.js +213,Bulk Edit {0},Massen-Bearbeitung {0} DocType: Email Alert Recipient,Email Alert Recipient,E-Mail-Benachrichtigungsempfänger DocType: About Us Settings,Settings for the About Us Page,"Einstellungen für die Seite ""Über uns""" apps/frappe/frappe/core/page/data_import_tool/data_import_main.html +5,Select Type of Document to Download,Wählen Sie die Art des Dokumentes zum Download @@ -995,10 +995,10 @@ sites/assets/js/desk.min.js +874,Expand,erweitern DocType: Workflow State,align-right,rechtsbündig ausrichten DocType: Page,Roles,Rollen DocType: System Settings,Session Expiry,Sitzungsende -DocType: Workflow State,ban-circle,ban-circle +DocType: Workflow State,ban-circle,Bannkreis DocType: Event,Desk,Schreibtisch apps/frappe/frappe/core/doctype/report/report.js +8,Write a SELECT query. Note result is not paged (all data is sent in one go).,Eine SELECT-Abfrage schreiben. Das Ergebnis wird nicht ausgelagert (alle Daten werden in einem Rutsch gesendet). -DocType: Email Account,Attachment Limit (MB),Limit Anhangsgröße (MB) +DocType: Email Account,Attachment Limit (MB),Beschränkung der Größe des Anhangs (MB) sites/assets/js/form.min.js +199,Ctrl + Down,Ctrl + Down DocType: User,User Defaults,Profil Defaults DocType: Workflow State,chevron-down,Chevron-nach-unten @@ -1007,8 +1007,8 @@ DocType: Web Page,Enable Comments,Kommentare aktivieren DocType: User,Restrict user from this IP address only. Multiple IP addresses can be added by separating with commas. Also accepts partial IP addresses like (111.111.111),Nur Benutzer von dieser IP-Adresse beschränken. Mehrere IP-Adressen können durch Trennung mit Komma hinzugefügt werden. Auch übernimmt teilweise IP-Adressen wie (111.111.111) DocType: Website Theme,Google Font (Heading),Google Font (Heading) sites/assets/js/desk.min.js +931,Find {0} in {1},Finden Sie in {0} {1} -DocType: Email Account,"Append as communication against this DocType (must have fields, ""Status"", ""Subject"")","Hängen Sie die Kommunikation gegen diese DocType (müssen Felder, ""Status"" haben, ""Subject"")" -DocType: DocType,Allow Import via Data Import Tool,Import über Datenimport-Tool zulassen +DocType: Email Account,"Append as communication against this DocType (must have fields, ""Status"", ""Subject"")","Als Kommunikation mit diesem Dokumententyp anhängen (muss die Felder, ""Status"" und ""Betreff"" beinhalten)" +DocType: DocType,Allow Import via Data Import Tool,Import über Datenimport-Werkzeug zulassen apps/frappe/frappe/model/base_document.py +432,Not allowed to change {0} after submission,"Nicht erlaubt, um nach Vorlage ändern {0}" DocType: Comment,Comment Type,Kommentarart apps/frappe/frappe/config/setup.py +8,Users,Benutzer @@ -1024,14 +1024,14 @@ sites/assets/js/desk.min.js +965,Send As Email,Senden als E-Mail DocType: Website Theme,Link Color,Linkfarbe apps/frappe/frappe/core/doctype/user/user.py +47,User {0} cannot be disabled,Benutzer {0} kann nicht deaktiviert werden apps/frappe/frappe/core/doctype/user/user.py +479,"Dear System Manager,","Lieber System Manager," -sites/assets/js/form.min.js +182,Amending,Zur Änderung der +sites/assets/js/form.min.js +182,Amending,Änderung sites/assets/js/desk.min.js +598,Dialog box to select a Link Value,Dialog-Box um einen verknpften Wert auszuwählen DocType: Contact Us Settings,Send enquiries to this email address,Senden Sie Anfragen an diese E-Mail -Adresse DocType: Letter Head,Letter Head Name,Briefkopf Name apps/frappe/frappe/config/website.py +23,User editable form on Website.,Benutzer bearbeitbare Formular auf der Website. DocType: Workflow State,file,Datei apps/frappe/frappe/model/rename_doc.py +97,You need write permission to rename,Zum Umbenennen benötigen Sie eine Schreibberechtigung -apps/frappe/frappe/core/page/permission_manager/permission_manager.js +433,Apply Rule,Apply Rule +apps/frappe/frappe/core/page/permission_manager/permission_manager.js +433,Apply Rule,Regel anwenden DocType: User,Karma,Karma DocType: DocField,Table,Tabelle DocType: File Data,File Size,Dateigröße @@ -1045,7 +1045,7 @@ DocType: Web Form,Allow Edit,Bearbeitung zulassen apps/frappe/frappe/workflow/doctype/workflow/workflow.py +64,Cannot change state of Cancelled Document. Transition row {0},Zustand der Cancelled Dokument kann nicht geändert werden . DocType: Workflow,"Rules for how states are transitions, like next state and which role is allowed to change state etc.","Regeln dafür, wann Status zu Übergängen zählen und welche Rolle Status ändern darf usw." apps/frappe/frappe/desk/form/save.py +21,{0} {1} already exists,{0} {1} existiert bereits -apps/frappe/frappe/email/doctype/email_account/email_account.py +61,Append To can be one of {0},"Hängen, um einen von {0}" +apps/frappe/frappe/email/doctype/email_account/email_account.py +61,Append To can be one of {0},Anhängen an kann ein Wert aus {0} sein DocType: User,Github Username,github Benutzername DocType: Web Page,Title / headline of your page,Titel/Überschrift Ihrer Seite DocType: DocType,Plugin,Plugin @@ -1066,7 +1066,7 @@ sites/assets/js/desk.min.js +579,Advanced Search,Erweiterte Suche apps/frappe/frappe/core/doctype/user/user.py +390,Password reset instructions have been sent to your email,Kennworts Hinweise wurden an die E-Mail verschickt worden apps/frappe/frappe/config/setup.py +214,Manage cloud backups on Dropbox,Verwalten Cloud -Backups auf Dropbox DocType: Workflow,States,Länder -DocType: Email Alert,Attach Print,Attach Print +DocType: Email Alert,Attach Print,Ausdruck anhängen apps/frappe/frappe/core/page/permission_manager/permission_manager_help.html +18,"When you Amend a document after Cancel and save it, it will get a new number that is a version of the old number.","Wenn Sie ein Dokument ändern, nachdem Sie auf Abbrechen und Speichern geklickt haben, wird es eine neue Nummer erhalten, die eine Version der alten Nummer ist, bekommen." apps/frappe/frappe/core/doctype/doctype/doctype.py +36,{0} not allowed in name,{0} nicht im Namen erlaubt DocType: Workflow State,circle-arrow-left,Kreis-Pfeil-nach-links @@ -1094,7 +1094,7 @@ DocType: Workflow State,folder-close,geschlossener Ordner DocType: Email Alert Recipient,Optional: Alert will only be sent if value is a valid email id.,"Optional: Alarm wird nur gesendet werden, wenn der Wert eine gültige E-Mail-Adresse." apps/frappe/frappe/model/rename_doc.py +100,{0} not allowed to be renamed,{0} darf nicht umbenannt werden DocType: Custom Script,Custom Script,Benutzerdefiniertes Skript -sites/assets/js/desk.min.js +622,Assigned To,zugewiesen an +sites/assets/js/desk.min.js +622,Assigned To,Zugewiesen zu apps/frappe/frappe/core/doctype/user/user.py +166,Verify Your Account,Überprüfen Sie Ihr Konto DocType: Workflow Transition,Action,Aktion apps/frappe/frappe/core/page/data_import_tool/exporter.py +231,Info:,Info: @@ -1157,7 +1157,7 @@ DocType: DocType,Name Case,Name in Großbuchstaben sites/assets/js/form.min.js +278,Shared with everyone,Mit allen geteilt apps/frappe/frappe/model/base_document.py +327,Data missing in table,In der Tabelle fehlen Daten DocType: Web Form,Success URL,Erfolgs-URL -DocType: Email Account,Append To,Anhängen Um +DocType: Email Account,Append To,Anhängen an DocType: Workflow Document State,Only Allow Edit For,Änderungen nur zulassen für DocType: DocType,DocType,Dokumententyp apps/frappe/frappe/core/doctype/user/user.py +393,User {0} does not exist,Benutzer {0} existiert nicht @@ -1180,7 +1180,7 @@ DocType: DocField,Set non-standard precision for a Float or Currency field,Stell DocType: Email Account,Ignore attachments over this size,Ignorieren Anhänge über dieser Größe apps/frappe/frappe/database.py +217,Too many writes in one request. Please send smaller requests,Zu viele schreibt in einer Anfrage. Bitte senden Sie Anfragen kleiner DocType: Workflow State,arrow-up,Pfeil-nach-oben -DocType: DocField,Allow on Submit,Beim Absenden zulassen +DocType: DocField,Allow on Submit,Beim Versenden zulassen apps/frappe/frappe/core/page/desktop/desktop.js +48,All Applications,Alle Anwendungen DocType: Web Page,Add code as <script>,Hinzufügen von Code als <script> apps/frappe/frappe/core/page/data_import_tool/data_import_main.html +7,Select Type,Typ auswählen @@ -1217,7 +1217,7 @@ DocType: Workflow State,star-empty,Stern-leer DocType: Workflow State,ok,OK DocType: User,These values will be automatically updated in transactions and also will be useful to restrict permissions for this user on transactions containing these values.,"Diese Werte werden automatisch bei Transaktionen aktualisiert. Außerdem sind sie nützlich, um die Berechtigungen dieses Benutzers bei Transaktionen mit diesen Werten zu beschränken." apps/frappe/frappe/desk/page/applications/application_row.html +15,Publisher,Herausgeber -sites/assets/js/desk.min.js +846,Browse,Blättern +sites/assets/js/desk.min.js +846,Browse,Durchsuchen apps/frappe/frappe/templates/includes/comments/comments.py +52,View it in your browser,Sehen Sie es in Ihrem Browser apps/frappe/frappe/templates/pages/update-password.html +1,Reset Password,Kennwort zurücksetzen DocType: Workflow State,hand-left,Pfeil-nach-links @@ -1247,7 +1247,7 @@ apps/frappe/frappe/core/page/permission_manager/permission_manager.js +474,Did n DocType: ToDo,ToDo,Aufgabenliste DocType: DocField,No Copy,Keine Kopie DocType: Workflow State,qrcode,qrcode -DocType: Web Form,Breadcrumbs,Paniermehl +DocType: Web Form,Breadcrumbs,Brösel apps/frappe/frappe/core/doctype/doctype/doctype.py +373,If Owner,Wenn Besitzer apps/frappe/frappe/website/doctype/web_form/web_form.py +28,You need to be logged in to access this {0}.,"Sie müssen angemeldet sein, um diesen Zugang {0}." apps/frappe/frappe/templates/includes/comments/comments.py +50,{0} by {1},{0} von {1} @@ -1282,7 +1282,7 @@ apps/frappe/frappe/print/page/print_format_builder/print_format_builder.js +102, DocType: Module Def,Module Def,Modul-Def sites/assets/js/form.min.js +199,Done,Erledigt sites/assets/js/form.min.js +294,Reply,Antworten -DocType: Communication,By,Nach +DocType: Communication,By,mit DocType: Email Account,SMTP Server,SMTP-Server DocType: Print Format,Print Format Help,Print-Format Hilfe apps/frappe/frappe/core/page/data_import_tool/exporter.py +70,"If you are updating, please select ""Overwrite"" else existing rows will not be deleted.","Wenn Sie aktualisieren, wählen Sie ""Überschreiben"" sonst vorhandene Zeilen werden nicht gelöscht." @@ -1297,7 +1297,7 @@ DocType: Event,Every Year,Jährlich apps/frappe/frappe/core/doctype/user/user.py +359,Registered but disabled.,"Registriert, aber deaktiviert." sites/assets/js/list.min.js +95,Delete permanently?,Dauerhaft löschen? sites/assets/js/desk.min.js +922,Search in a document type,Suche in einem Dokumenttyp -apps/frappe/frappe/custom/doctype/customize_form/customize_form.js +201,Allow field to remain editable even after submission,Lassen Feld auch nach Vorlage bearbeitet bleiben +apps/frappe/frappe/custom/doctype/customize_form/customize_form.js +201,Allow field to remain editable even after submission,"Zulassen, dass Feld bearbeitbar bleibt, auch nach Einreichung" DocType: DocPerm,Role and Level,Rolle und Ebene apps/frappe/frappe/desk/moduleview.py +31,Custom Reports,Benutzerdefinierte Berichte DocType: Website Script,Website Script,Webseitenskript @@ -1322,7 +1322,7 @@ apps/frappe/frappe/core/page/permission_manager/permission_manager_help.html +32 apps/frappe/frappe/workflow/doctype/workflow/workflow.py +70,Cannot cancel before submitting. See Transition {0},Kann nicht vor der Vorlage zu stornieren. apps/frappe/frappe/templates/pages/print.py +146,Print Format {0} is disabled,Druckformat {0} ist deaktiviert DocType: Email Alert,Send days before or after the reference date,Senden Sie Tage vor oder nach dem Stichtag -DocType: User,Allow user to login only after this hour (0-24),"Benutzer erlauben, sich nach dieser Stunde anzumelden (0-24)" +DocType: User,Allow user to login only after this hour (0-24),"Benutzer erlauben, sich erst nach dieser Stunde anzumelden (0-24)" apps/frappe/frappe/core/doctype/doctype/doctype.py +33,Not in Developer Mode! Set in site_config.json or make 'Custom' DocType.,"Nicht in Entwicklermodus! In site_config.json erstellen können und ""Benutzerdefiniert"" DocType." DocType: Workflow State,globe,Globus DocType: System Settings,dd.mm.yyyy,TT.MM.JJJJ @@ -1368,7 +1368,7 @@ DocType: Web Form,"In JSON as
    [{""title"":""Jobs"", ""name"":""jobs""}]
    SrSr + {{ _(tdf.label) }}
    {{ d.idx }}{{ d.idx }} -
    {{ print_value(tdf, d, doc) }}
    +
    {{ print_value(tdf, d, doc) }}