Merge pull request #21638 from MyuddinKhatri/lead-update-fix-v13

fix(crm): fix lead while updating contact details (develop)
diff --git a/erpnext/accounts/doctype/payment_order/payment_order.py b/erpnext/accounts/doctype/payment_order/payment_order.py
index 3f3174a..7ecdc41 100644
--- a/erpnext/accounts/doctype/payment_order/payment_order.py
+++ b/erpnext/accounts/doctype/payment_order/payment_order.py
@@ -80,7 +80,7 @@
 			paid_amt += d.amount
 
 	je.append('accounts', {
-		'account': doc.references[0].account,
+		'account': doc.account,
 		'credit_in_account_currency': paid_amt
 	})
 
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index 74b3582..ec7d14d 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -153,7 +153,7 @@
 		if not self.lead_name:
 			self.set_lead_name()
 
-		names = self.lead_name.split(" ")
+		names = self.lead_name.strip().split(" ")
 		if len(names) > 1:
 			first_name, last_name = names[0], " ".join(names[1:])
 		else:
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
index 50b98e9..263005e 100644
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
@@ -62,6 +62,8 @@
 				callback : function(r) {
 					window.location.href = r.message;
 				}
+			}).fail(function() {
+				frappe.dom.unfreeze();
 			});
 		}
 	},
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
index 5df35df..bdde9ee 100644
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
@@ -15,7 +15,7 @@
 		params = urlencode({
 			"response_type":"code",
 			"client_id": self.consumer_key,
-			"redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback",
+			"redirect_uri": get_site_url(frappe.local.site) + "/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?",
 			"scope": "r_emailaddress w_organization_social r_basicprofile r_liteprofile r_organization_social rw_organization_admin w_member_social"
 		})
 
@@ -30,7 +30,7 @@
 			"code": code,
 			"client_id": self.consumer_key,
 			"client_secret": self.get_password(fieldname="consumer_secret"),
-			"redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback",
+			"redirect_uri": get_site_url(frappe.local.site) + "/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?",
 		}
 		headers = {
 			"Content-Type": "application/x-www-form-urlencoded"
@@ -154,7 +154,7 @@
 
 		return response
 
-@frappe.whitelist()
+@frappe.whitelist(allow_guest=True)
 def callback(code=None, error=None, error_description=None):
 	if not error:
 		linkedin_settings = frappe.get_doc("LinkedIn Settings")
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js
index b55946a..f6f431c 100644
--- a/erpnext/crm/doctype/twitter_settings/twitter_settings.js
+++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.js
@@ -47,6 +47,8 @@
 				callback : function(r) {
 					window.location.href = r.message;
 				}
+			}).fail(function() {
+				frappe.dom.unfreeze();
 			});
 		}
 	},
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py
index 64f53b5..7616b4c 100644
--- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py
+++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py
@@ -12,13 +12,12 @@
 
 class TwitterSettings(Document):
 	def get_authorize_url(self):
-		callback_url = "{0}/?cmd=erpnext.crm.doctype.twitter_settings.twitter_settings.callback".format(frappe.utils.get_url())
+		callback_url = "{0}/api/method/erpnext.crm.doctype.twitter_settings.twitter_settings.callback?".format(frappe.utils.get_url())
 		auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url)
-
 		try:
 			redirect_url = auth.get_authorization_url()
 			return redirect_url
-		except:
+		except tweepy.TweepError as e:
 			frappe.msgprint(_("Error! Failed to get request token."))
 			frappe.throw(_('Invalid {0} or {1}').format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key")))
 
@@ -91,8 +90,12 @@
 				frappe.db.commit()
 			frappe.throw(content["message"],title="Twitter Error {0} {1}".format(e.response.status_code, e.response.reason))
 
-@frappe.whitelist()
-def callback(oauth_token, oauth_verifier):
-	twitter_settings = frappe.get_single("Twitter Settings")
-	twitter_settings.get_access_token(oauth_token,oauth_verifier)
-	frappe.db.commit()
+@frappe.whitelist(allow_guest=True)
+def callback(oauth_token = None, oauth_verifier = None):
+	if oauth_token and oauth_verifier:
+		twitter_settings = frappe.get_single("Twitter Settings")
+		twitter_settings.get_access_token(oauth_token,oauth_verifier)
+		frappe.db.commit()
+	else:
+		frappe.local.response["type"] = "redirect"
+		frappe.local.response["location"] = get_url_to_form("Twitter Settings","Twitter Settings")
diff --git a/erpnext/education/doctype/education_settings/education_settings.json b/erpnext/education/doctype/education_settings/education_settings.json
index 967a030..0e548db 100644
--- a/erpnext/education/doctype/education_settings/education_settings.json
+++ b/erpnext/education/doctype/education_settings/education_settings.json
@@ -1,4 +1,5 @@
 {
+ "actions": [],
  "creation": "2017-04-05 13:33:04.519313",
  "doctype": "DocType",
  "editable_grid": 1,
@@ -42,12 +43,14 @@
    "fieldtype": "Column Break"
   },
   {
+   "default": "0",
    "description": "For Batch based Student Group, the Student Batch will be validated for every Student from the Program Enrollment.",
    "fieldname": "validate_batch",
    "fieldtype": "Check",
    "label": "Validate Batch for Students in Student Group"
   },
   {
+   "default": "0",
    "description": "For Course based Student Group, the Course will be validated for every Student from the enrolled Courses in Program Enrollment.",
    "fieldname": "validate_course",
    "fieldtype": "Check",
@@ -74,13 +77,13 @@
   {
    "fieldname": "web_academy_settings_section",
    "fieldtype": "Section Break",
-   "label": "LMS Settings"
+   "label": "Learning Management System Settings"
   },
   {
    "depends_on": "eval: doc.enable_lms",
    "fieldname": "portal_title",
    "fieldtype": "Data",
-   "label": "LMS Title"
+   "label": "Learning Management System Title"
   },
   {
    "depends_on": "eval: doc.enable_lms",
@@ -89,9 +92,10 @@
    "label": "Description"
   },
   {
+   "default": "0",
    "fieldname": "enable_lms",
    "fieldtype": "Check",
-   "label": "Enable LMS"
+   "label": "Enable Learning Management System"
   },
   {
    "default": "0",
@@ -102,7 +106,8 @@
   }
  ],
  "issingle": 1,
- "modified": "2019-05-13 18:36:13.127563",
+ "links": [],
+ "modified": "2020-05-07 19:18:10.639356",
  "modified_by": "Administrator",
  "module": "Education",
  "name": "Education Settings",
@@ -141,4 +146,4 @@
  "sort_field": "modified",
  "sort_order": "DESC",
  "track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index c550d49..76e10e5 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -248,8 +248,7 @@
 	for loan_security in loan_security_pledge_details:
 		unpledge_request.append('securities', {
 			"loan_security": loan_security.loan_security,
-			"qty": loan_security.qty,
-			"against_pledge": loan_security.parent
+			"qty": loan_security.qty
 		})
 
 	if as_dict:
diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py
index 77a1fcc..364e2ff 100644
--- a/erpnext/loan_management/doctype/loan/test_loan.py
+++ b/erpnext/loan_management/doctype/loan/test_loan.py
@@ -15,6 +15,7 @@
 from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year
 from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall
 from erpnext.loan_management.doctype.loan.loan import create_loan_security_unpledge
+from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
 
 class TestLoan(unittest.TestCase):
 	def setUp(self):
@@ -152,7 +153,7 @@
 		repayment_entry.save()
 		repayment_entry.submit()
 
-		penalty_amount = (accrued_interest_amount * 5 * 25) / (100 * days_in_year(get_datetime(first_date).year))
+		penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year))
 		self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2))
 
 		amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
@@ -305,7 +306,7 @@
 		make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
 		process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
 
-		repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
+		repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6),
 			"Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
 		repayment_entry.submit()
 
@@ -319,13 +320,12 @@
 		unpledge_request.submit()
 		unpledge_request.status = 'Approved'
 		unpledge_request.save()
-
-		loan_security_pledge.load_from_db()
 		loan.load_from_db()
 
+		pledged_qty = get_pledged_security_qty(loan.name)
+
 		self.assertEqual(loan.status, 'Closed')
-		for security in loan_security_pledge.securities:
-			self.assertEquals(security.qty, 0)
+		self.assertEquals(sum(pledged_qty.values()), 0)
 
 
 def create_loan_accounts():
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 452c836..2ab668a 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -264,6 +264,7 @@
 	penalty_amount = 0
 	payable_principal_amount = 0
 	final_due_date = ''
+	due_date = ''
 
 	for entry in accrued_interest_entries:
 		# Loan repayment due date is one day after the loan interest is accrued
@@ -272,7 +273,7 @@
 
 		due_date = add_days(entry.posting_date, 1)
 		no_of_late_days = date_diff(posting_date,
-					add_days(due_date, loan_type_details.grace_period_in_days)) + 1
+					add_days(due_date, loan_type_details.grace_period_in_days))
 
 		if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary):
 			penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365
@@ -290,9 +291,9 @@
 
 	pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable
 
-	if payment_type == "Loan Closure" and not payable_principal_amount:
-		if final_due_date:
-			pending_days = date_diff(posting_date, final_due_date)
+	if payment_type == "Loan Closure":
+		if due_date:
+			pending_days = date_diff(posting_date, due_date) + 1
 		else:
 			pending_days = date_diff(posting_date, against_loan_doc.disbursement_date) + 1
 
diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
index 8ca6e3e..308c438 100644
--- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
+++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
@@ -69,7 +69,7 @@
 		loan_security_map[loan.name]['security_value'] += current_loan_security_amount - (current_loan_security_amount * loan.haircut/100)
 
 	for loan, value in iteritems(loan_security_map):
-		if (value["security_value"]/value["loan_amount"]) < ltv_ratio:
+		if (value["loan_amount"]/value['security_value'] * 100) > ltv_ratio:
 			create_loan_security_shortfall(loan, value, process_loan_security_shortfall)
 
 def create_loan_security_shortfall(loan, value, process_loan_security_shortfall):
diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js
index 72c5f38..8223206 100644
--- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js
+++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js
@@ -4,10 +4,8 @@
 frappe.ui.form.on('Loan Security Unpledge', {
 	refresh: function(frm) {
 
-		frm.set_query("against_pledge", "securities", () => {
-			return {
-				filters : [["status", "in", ["Pledged", "Partially Pledged"]]]
-			};
-		});
+		if (frm.doc.docstatus == 1 && frm.doc.status == 'Approved') {
+			frm.set_df_property('status', 'read_only', 1);
+		}
 	}
 });
diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
index b2bb22a..5e9d82a 100644
--- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
+++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
@@ -8,12 +8,13 @@
 from frappe.model.document import Document
 from frappe.utils import get_datetime, flt
 import json
+from six import iteritems
 from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price
 
 class LoanSecurityUnpledge(Document):
 	def validate(self):
-		self.validate_pledges()
 		self.validate_duplicate_securities()
+		self.validate_unpledge_qty()
 
 	def on_cancel(self):
 		self.update_loan_security_pledge(cancel=1)
@@ -23,80 +24,52 @@
 	def validate_duplicate_securities(self):
 		security_list = []
 		for d in self.securities:
-			security = [d.loan_security, d.against_pledge]
-			if security not in security_list:
-				security_list.append(security)
+			if d.loan_security not in security_list:
+				security_list.append(d.loan_security)
 			else:
-				frappe.throw(_("Row {0}: Loan Security {1} against Loan Security Pledge {2} added multiple times").format(
-					d.idx, frappe.bold(d.loan_security), frappe.bold(d.against_pledge)))
+				frappe.throw(_("Row {0}: Loan Security {1} added multiple times").format(
+					d.idx, frappe.bold(d.loan_security)))
 
-	def validate_pledges(self):
-		pledge_qty_map = self.get_pledge_details()
-		loan = frappe.get_doc("Loan", self.loan)
+	def validate_unpledge_qty(self):
+		pledge_qty_map = get_pledged_security_qty(self.loan)
 
-		remaining_qty = 0
-		unpledge_value = 0
+		ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type",
+			fields=["name", "loan_to_value_ratio"], as_list=1))
+
+		loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price",
+			fields=["loan_security", "loan_security_price"],
+			filters = {
+				"valid_from": ("<=", get_datetime()),
+				"valid_upto": (">=", get_datetime())
+			}, as_list=1))
+
+		loan_amount, principal_paid = frappe.get_value("Loan", self.loan, ['loan_amount', 'total_principal_paid'])
+		pending_principal_amount = loan_amount - principal_paid
+		security_value = 0
 
 		for security in self.securities:
-			pledged_qty = pledge_qty_map.get((security.against_pledge, security.loan_security), 0)
-			if not pledged_qty:
-				frappe.throw(_("Zero qty of {0} pledged against loan {1}").format(frappe.bold(security.loan_security),
-					frappe.bold(self.loan)))
+			pledged_qty = pledge_qty_map.get(security.loan_security)
 
-			unpledge_qty = pledged_qty - security.qty
-			security_price = security.qty * get_loan_security_price(security.loan_security)
+			if security.qty > pledged_qty:
+				frappe.throw(_("""Row {0}: {1} {2} of {3} is pledged against Loan {4}.
+					You are trying to unpledge more""").format(security.idx, pledged_qty, security.uom,
+					frappe.bold(security.loan_security), frappe.bold(self.loan)))
 
-			if unpledge_qty < 0:
-				frappe.throw(_("""Row {0}: Cannot unpledge more than {1} qty of {2} against
-					Loan Security Pledge {3}""").format(security.idx, frappe.bold(pledged_qty),
-					frappe.bold(security.loan_security), frappe.bold(security.against_pledge)))
+			qty_after_unpledge = pledged_qty - security.qty
+			ltv_ratio = ltv_ratio_map.get(security.loan_security_type)
 
-			remaining_qty += unpledge_qty
-			unpledge_value += security_price - flt(security_price * security.haircut/100)
+			security_value += qty_after_unpledge * loan_security_price_map.get(security.loan_security)
 
-		if unpledge_value > loan.total_principal_paid:
-			frappe.throw(_("Cannot Unpledge, loan security value is greater than the repaid amount"))
+		if not security_value and pending_principal_amount > 0:
+			frappe.throw("Cannot Unpledge, loan to value ratio is breaching")
 
-	def get_pledge_details(self):
-		pledge_qty_map = {}
-
-		pledge_details = frappe.db.sql("""
-			SELECT p.parent, p.loan_security, p.qty FROM
-				`tabLoan Security Pledge` lsp,
-				`tabPledge` p
-			WHERE
-				p.parent = lsp.name
-				AND lsp.loan = %s
-				AND lsp.docstatus = 1
-				AND lsp.status in ('Pledged', 'Partially Pledged')
-		""", (self.loan), as_dict=1)
-
-		for pledge in pledge_details:
-			pledge_qty_map.setdefault((pledge.parent, pledge.loan_security), pledge.qty)
-
-		return pledge_qty_map
+		if security_value and (pending_principal_amount/security_value) * 100 > ltv_ratio:
+			frappe.throw("Cannot Unpledge, loan to value ratio is breaching")
 
 	def on_update_after_submit(self):
 		if self.status == "Approved":
-			self.update_loan_security_pledge()
 			self.update_loan_status()
-
-	def update_loan_security_pledge(self, cancel=0):
-		if cancel:
-			new_qty = 'p.qty + u.qty'
-		else:
-			new_qty = 'p.qty - u.qty'
-
-		frappe.db.sql("""
-			UPDATE
-				`tabPledge` p, `tabUnpledge` u, `tabLoan Security Pledge` lsp, `tabLoan Security Unpledge` lsu
-					SET p.qty = {new_qty}
-			WHERE
-				lsp.loan = %s
-				AND p.parent = u.against_pledge
-				AND p.parent = lsp.name
-				AND lsp.docstatus = 1
-				AND p.loan_security = u.loan_security""".format(new_qty=new_qty),(self.loan))
+			self.db_set('unpledge_time', get_datetime())
 
 	def update_loan_status(self, cancel=0):
 		if cancel:
@@ -104,10 +77,45 @@
 			if loan_status == 'Closed':
 				frappe.db.set_value('Loan', self.loan, 'status', 'Loan Closure Requested')
 		else:
-			pledge_qty = frappe.db.sql("""SELECT SUM(c.qty)
-				FROM `tabLoan Security Pledge` p, `tabPledge` c
-				WHERE p.loan = %s AND c.parent = p.name""", (self.loan))[0][0]
+			pledged_qty = 0
+			current_pledges = get_pledged_security_qty(self.loan)
 
-			if not pledge_qty:
+			for security, qty in iteritems(current_pledges):
+				pledged_qty += qty
+
+			if not pledged_qty:
 				frappe.db.set_value('Loan', self.loan, 'status', 'Closed')
 
+@frappe.whitelist()
+def get_pledged_security_qty(loan):
+
+	current_pledges = {}
+
+	unpledges = frappe._dict(frappe.db.sql("""
+		SELECT u.loan_security, sum(u.qty) as qty
+		FROM `tabLoan Security Unpledge` up, `tabUnpledge` u
+		WHERE up.loan = %s
+		AND u.parent = up.name
+		AND up.status = 'Approved'
+		GROUP BY u.loan_security
+	""", (loan)))
+
+	pledges = frappe._dict(frappe.db.sql("""
+		SELECT p.loan_security, sum(p.qty) as qty
+		FROM `tabLoan Security Pledge` lp, `tabPledge`p
+		WHERE lp.loan = %s
+		AND p.parent = lp.name
+		AND lp.status = 'Pledged'
+		GROUP BY p.loan_security
+	""", (loan)))
+
+	for security, qty in iteritems(pledges):
+		current_pledges.setdefault(security, qty)
+		current_pledges[security] -= unpledges.get(security, 0.0)
+
+	return current_pledges
+
+
+
+
+
diff --git a/erpnext/loan_management/doctype/unpledge/unpledge.json b/erpnext/loan_management/doctype/unpledge/unpledge.json
index 9e6277d..ee192d7 100644
--- a/erpnext/loan_management/doctype/unpledge/unpledge.json
+++ b/erpnext/loan_management/doctype/unpledge/unpledge.json
@@ -1,11 +1,11 @@
 {
+ "actions": [],
  "creation": "2019-09-21 13:22:19.793797",
  "doctype": "DocType",
  "editable_grid": 1,
  "engine": "InnoDB",
  "field_order": [
   "loan_security",
-  "against_pledge",
   "loan_security_type",
   "loan_security_code",
   "haircut",
@@ -55,14 +55,6 @@
    "reqd": 1
   },
   {
-   "fieldname": "against_pledge",
-   "fieldtype": "Link",
-   "in_list_view": 1,
-   "label": "Against Pledge",
-   "options": "Loan Security Pledge",
-   "reqd": 1
-  },
-  {
    "fetch_from": "loan_security.haircut",
    "fieldname": "haircut",
    "fieldtype": "Percent",
@@ -71,7 +63,8 @@
   }
  ],
  "istable": 1,
- "modified": "2019-10-02 12:48:18.588236",
+ "links": [],
+ "modified": "2020-05-06 10:50:18.448552",
  "modified_by": "Administrator",
  "module": "Loan Management",
  "name": "Unpledge",
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 84bfab2..8301f30 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -421,6 +421,9 @@
 		return holidays[holiday_list]
 
 	def update_operation_status(self):
+		allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order"))
+		max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage/100 * flt(self.qty))
+
 		for d in self.get("operations"):
 			if not d.completed_qty:
 				d.status = "Pending"
@@ -428,6 +431,8 @@
 				d.status = "Work in Progress"
 			elif flt(d.completed_qty) == flt(self.qty):
 				d.status = "Completed"
+			elif flt(d.completed_qty) <= max_allowed_qty_for_wo:
+				d.status = "Completed"
 			else:
 				frappe.throw(_("Completed Qty can not be greater than 'Qty to Manufacture'"))
 
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index ce0e4ac..5255933 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -680,3 +680,4 @@
 erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry
 erpnext.patches.v12_0.retain_permission_rules_for_video_doctype
 erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive
+execute:frappe.delete_doc_if_exists("Page", "appointment-analytic")