diff --git a/.eslintrc b/.eslintrc index 937f11586c..adc4aebb28 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,7 +5,7 @@ "es6": true }, "parserOptions": { - "ecmaVersion": 9, + "ecmaVersion": 11, "sourceType": "module" }, "extends": "eslint:recommended", diff --git a/.github/helper/install_dependencies.sh b/.github/helper/install_dependencies.sh index d16f5b62ad..f0e8016860 100644 --- a/.github/helper/install_dependencies.sh +++ b/.github/helper/install_dependencies.sh @@ -2,6 +2,13 @@ set -e +# Check for merge conflicts before proceeding +python -m compileall -f "${GITHUB_WORKSPACE}" +if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" + then echo "Found merge conflicts" + exit 1 +fi + # install wkhtmltopdf wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz tar -xf /tmp/wkhtmltox.tar.xz -C /tmp diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a6c1243f64..9c7ecf989e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,6 @@ jobs: npm install @semantic-release/git @semantic-release/exec --no-save - name: Create Release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} GIT_AUTHOR_NAME: "Frappe PR Bot" diff --git a/frappe/__init__.py b/frappe/__init__.py index 07f75ecd31..d54686304f 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -199,6 +199,7 @@ def init(site, sites_path=None, new_site=False): } ) local.rollback_observers = [] + local.locked_documents = [] local.before_commit = [] local.test_objects = {} @@ -1507,10 +1508,11 @@ def get_newargs(fn, kwargs): if hasattr(fn, "fnargs"): fnargs = fn.fnargs else: - fullargspec = inspect.getfullargspec(fn) - fnargs = fullargspec.args - fnargs.extend(fullargspec.kwonlyargs) - varkw = fullargspec.varkw + signature = inspect.signature(fn) + fnargs = list(signature.parameters) + varkw = "kwargs" in fnargs + if varkw: + fnargs.pop(-1) newargs = {} for a in kwargs: @@ -2263,7 +2265,4 @@ def mock(type, size=1, locale="en"): return squashify(results) -def validate_and_sanitize_search_inputs(fn): - from frappe.desk.search import validate_and_sanitize_search_inputs as func - - return func(fn) +from frappe.desk.search import validate_and_sanitize_search_inputs # noqa diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 548d21bb60..9312ae178b 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -49,7 +49,7 @@ "fieldname": "doctype_event", "fieldtype": "Select", "label": "DocType Event", - "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)" + "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)\nOn Payment Authorization" }, { "depends_on": "eval:doc.script_type==='API'", @@ -109,7 +109,7 @@ "link_fieldname": "server_script" } ], - "modified": "2022-04-07 19:41:23.178772", + "modified": "2022-04-27 11:42:52.032963", "modified_by": "Administrator", "module": "Core", "name": "Server Script", diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index 5300baa199..b807b43d10 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -17,6 +17,7 @@ EVENT_MAP = { "after_delete": "After Delete", "before_update_after_submit": "Before Save (Submitted Document)", "on_update_after_submit": "After Save (Submitted Document)", + "on_payment_authorized": "On Payment Authorization", } diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index 1e79bf67d8..2cae3ab82f 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -240,7 +240,7 @@ CREATE TABLE "tabDocType" ( DROP TABLE IF EXISTS "tabSeries"; CREATE TABLE "tabSeries" ( - "name" varchar(100) DEFAULT NULL, + "name" varchar(100), "current" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("name") ) ; diff --git a/frappe/database/query.py b/frappe/database/query.py index 8d8a767370..136f5c86b6 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -108,11 +108,14 @@ def change_orderby(order: str): tuple: field, order """ order = order.split() - if order[1].lower() == "asc": - orderby, order = order[0], Order.asc - return orderby, order - orderby, order = order[0], Order.desc - return orderby, order + + try: + if order[1].lower() == "asc": + return order[0], Order.asc + except IndexError: + pass + + return order[0], Order.desc OPERATOR_MAP = { @@ -175,10 +178,13 @@ class Query: """ if kwargs.get("orderby"): orderby = kwargs.get("orderby") - order = kwargs.get("order") if kwargs.get("order") else Order.desc if isinstance(orderby, str) and len(orderby.split()) > 1: - orderby, order = change_orderby(orderby) - conditions = conditions.orderby(orderby, order=order) + for ordby in orderby.split(","): + if ordby := ordby.strip(): + orderby, order = change_orderby(ordby) + conditions = conditions.orderby(orderby, order=order) + else: + conditions = conditions.orderby(orderby, order=kwargs.get("order") or Order.desc) if kwargs.get("limit"): conditions = conditions.limit(kwargs.get("limit")) @@ -288,7 +294,7 @@ class Query: table: str, fields: Union[List, Tuple], filters: Union[Dict[str, Union[str, int]], str, int] = None, - **kwargs + **kwargs, ): criterion = self.build_conditions(table, filters, **kwargs) if isinstance(fields, (list, tuple)): diff --git a/frappe/desk/search.py b/frappe/desk/search.py index ba4c5fb4fb..eb1a2e82ba 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -1,12 +1,10 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +import functools import json import re -import wrapt - -# Search import frappe from frappe import _, is_whitelisted from frappe.permissions import has_permission @@ -314,17 +312,20 @@ def relevance_sorter(key, query, as_dict): return (cstr(value).lower().startswith(query.lower()) is not True, value) -@wrapt.decorator -def validate_and_sanitize_search_inputs(fn, instance, args, kwargs): - kwargs.update(dict(zip(fn.__code__.co_varnames, args))) - sanitize_searchfield(kwargs["searchfield"]) - kwargs["start"] = cint(kwargs["start"]) - kwargs["page_len"] = cint(kwargs["page_len"]) +def validate_and_sanitize_search_inputs(fn): + @functools.wraps(fn) + def wrapper(*args, **kwargs): + kwargs.update(dict(zip(fn.__code__.co_varnames, args))) + sanitize_searchfield(kwargs["searchfield"]) + kwargs["start"] = cint(kwargs["start"]) + kwargs["page_len"] = cint(kwargs["page_len"]) - if kwargs["doctype"] and not frappe.db.exists("DocType", kwargs["doctype"]): - return [] + if kwargs["doctype"] and not frappe.db.exists("DocType", kwargs["doctype"]): + return [] - return fn(**kwargs) + return fn(**kwargs) + + return wrapper @frappe.whitelist() diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index 6aa881ed5c..b04ad4db40 100644 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -124,7 +124,7 @@ class Newsletter(WebsiteGenerator): ) def get_success_recipients(self) -> List[str]: - """Recipients who have already recieved the newsletter. + """Recipients who have already received the newsletter. Couldn't think of a better name ;) """ @@ -132,7 +132,7 @@ class Newsletter(WebsiteGenerator): "Email Queue Recipient", filters={ "status": ("in", ["Not Sent", "Sending", "Sent"]), - "parentfield": ("in", self.get_linked_email_queue()), + "parent": ("in", self.get_linked_email_queue()), }, pluck="recipient", ) diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py index c62b7e84aa..81702f3a09 100644 --- a/frappe/email/doctype/newsletter/test_newsletter.py +++ b/frappe/email/doctype/newsletter/test_newsletter.py @@ -221,3 +221,24 @@ class TestNewsletter(TestNewsletterMixin, unittest.TestCase): newsletter.reload() self.assertEqual(newsletter.email_sent, 0) + + def test_retry_partially_sent_newsletter(self): + frappe.db.delete("Email Queue") + frappe.db.delete("Email Queue Recipient") + frappe.db.delete("Newsletter") + + newsletter = self.get_newsletter() + newsletter.send_emails() + email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")] + self.assertEqual(len(email_queue_list), 4) + + # emulate partial send + email_queue_list[0].status = "Error" + email_queue_list[0].recipients[0].status = "Error" + email_queue_list[0].save() + newsletter.email_sent = False + + # retry + newsletter.send_emails() + email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")] + self.assertEqual(len(email_queue_list), 5) diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json index 23cadc2156..c1031fe211 100644 --- a/frappe/geo/country_info.json +++ b/frappe/geo/country_info.json @@ -954,6 +954,8 @@ "smallest_currency_fraction_value": 0.01, "currency_symbol": "\u20ac", "number_format": "#.###,##", + "date_format": "dd.mm.yyyy", + "time_format": "HH:mm", "timezones": [ "Europe/Berlin" ] diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 93446fb99e..186ef52c12 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -241,7 +241,6 @@ class BaseDocument(object): raise AttributeError(key) value = get_controller(value["doctype"])(value) - value.init_valid_columns() value.parent = self.name value.parenttype = self.doctype @@ -350,7 +349,7 @@ class BaseDocument(object): @property def docstatus(self): - return DocStatus(self.get("docstatus")) + return DocStatus(cint(self.get("docstatus"))) @docstatus.setter def docstatus(self, value): diff --git a/frappe/model/document.py b/frappe/model/document.py index 67e1de0932..c5e61563f8 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -989,6 +989,16 @@ class Document(BaseDocument): self.docstatus = DocStatus.cancelled() return self.save() + @whitelist.__func__ + def _rename( + self, name: str, merge: bool = False, force: bool = False, validate_rename: bool = True + ): + """Rename the document. Triggers frappe.rename_doc, then reloads.""" + from frappe.model.rename_doc import rename_doc + + self.name = rename_doc(doc=self, new=name, merge=merge, force=force, validate=validate_rename) + self.reload() + @whitelist.__func__ def submit(self): """Submit the document. Sets `docstatus` = 1, then saves.""" @@ -999,6 +1009,13 @@ class Document(BaseDocument): """Cancel the document. Sets `docstatus` = 2, then saves.""" return self._cancel() + @whitelist.__func__ + def rename( + self, name: str, merge: bool = False, force: bool = False, validate_rename: bool = True + ): + """Rename the document to `name`. This transforms the current object.""" + return self._rename(name=name, merge=merge, force=force, validate_rename=validate_rename) + def delete(self, ignore_permissions=False): """Delete document.""" frappe.delete_doc( @@ -1398,21 +1415,22 @@ class Document(BaseDocument): # See: Stock Reconciliation from frappe.utils.background_jobs import enqueue - if hasattr(self, "_" + action): - action = "_" + action + if hasattr(self, f"_{action}"): + action = f"_{action}" - if file_lock.lock_exists(self.get_signature()): + try: + self.lock() + except frappe.DocumentLockedError: frappe.throw( _("This document is currently queued for execution. Please try again"), title=_("Document Queued"), ) - self.lock() - enqueue( + return enqueue( "frappe.model.document.execute_action", - doctype=self.doctype, - name=self.name, - action=action, + __doctype=self.doctype, + __name=self.name, + __action=action, **kwargs, ) @@ -1433,10 +1451,13 @@ class Document(BaseDocument): if lock_exists: raise frappe.DocumentLockedError file_lock.create_lock(signature) + frappe.local.locked_documents.append(self) def unlock(self): """Delete the lock file for this document""" file_lock.delete_lock(self.get_signature()) + if self in frappe.local.locked_documents: + frappe.local.locked_documents.remove(self) # validation helpers def validate_from_to_dates(self, from_date_field, to_date_field): @@ -1495,12 +1516,12 @@ class Document(BaseDocument): return f"{doctype}({name})" -def execute_action(doctype, name, action, **kwargs): +def execute_action(__doctype, __name, __action, **kwargs): """Execute an action on a document (called by background worker)""" - doc = frappe.get_doc(doctype, name) + doc = frappe.get_doc(__doctype, __name) doc.unlock() try: - getattr(doc, action)(**kwargs) + getattr(doc, __action)(**kwargs) except Exception: frappe.db.rollback() @@ -1511,4 +1532,4 @@ def execute_action(doctype, name, action, **kwargs): msg = "
" + frappe.get_traceback() + ""
doc.add_comment("Comment", _("Action Failed") + "