Merge pull request #26558 from rohitwaghchaure/fixed-incorrect-gross-profit-report

fix: incorrect valuation rate calculation in gross profit report
diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json
index f190cfa..7c1baf1 100644
--- a/erpnext/non_profit/doctype/member/member.json
+++ b/erpnext/non_profit/doctype/member/member.json
@@ -26,7 +26,7 @@
   "razorpay_details_section",
   "subscription_id",
   "customer_id",
-  "subscription_activated",
+  "subscription_status",
   "column_break_21",
   "subscription_start",
   "subscription_end"
@@ -152,12 +152,6 @@
    "fieldtype": "Column Break"
   },
   {
-   "default": "0",
-   "fieldname": "subscription_activated",
-   "fieldtype": "Check",
-   "label": "Subscription Activated"
-  },
-  {
    "fieldname": "subscription_start",
    "fieldtype": "Date",
    "label": "Subscription Start "
@@ -166,11 +160,17 @@
    "fieldname": "subscription_end",
    "fieldtype": "Date",
    "label": "Subscription End"
+  },
+  {
+   "fieldname": "subscription_status",
+   "fieldtype": "Select",
+   "label": "Subscription Status",
+   "options": "\nActive\nHalted"
   }
  ],
  "image_field": "image",
  "links": [],
- "modified": "2020-11-09 12:12:10.174647",
+ "modified": "2021-07-11 14:27:26.368039",
  "modified_by": "Administrator",
  "module": "Non Profit",
  "name": "Member",
diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py
index 30be585..67828d6 100644
--- a/erpnext/non_profit/doctype/member/member.py
+++ b/erpnext/non_profit/doctype/member/member.py
@@ -84,7 +84,9 @@
 		"email_id": user_details.email,
 		"pan_number": user_details.pan or None,
 		"membership_type": user_details.plan_id,
-		"subscription_id": user_details.subscription_id or None
+		"customer_id": user_details.customer_id or None,
+		"subscription_id": user_details.subscription_id or None,
+		"subscription_status": user_details.subscription_status or ""
 	})
 
 	member.insert(ignore_permissions=True)
diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py
index e8ae618..b584116 100644
--- a/erpnext/non_profit/doctype/membership/membership.py
+++ b/erpnext/non_profit/doctype/membership/membership.py
@@ -196,11 +196,14 @@
 	return invoice
 
 
-def get_member_based_on_subscription(subscription_id, email):
-	members = frappe.get_all("Member", filters={
-					"subscription_id": subscription_id,
-					"email_id": email
-				}, order_by="creation desc")
+def get_member_based_on_subscription(subscription_id, email=None, customer_id=None):
+	filters = {"subscription_id": subscription_id}
+	if email:
+		filters.update({"email_id": email})
+	if customer_id:
+		filters.update({"customer_id": customer_id})
+
+	members = frappe.get_all("Member", filters=filters, order_by="creation desc")
 
 	try:
 		return frappe.get_doc("Member", members[0]["name"])
@@ -209,8 +212,6 @@
 
 
 def verify_signature(data, endpoint="Membership"):
-	if frappe.flags.in_test or os.environ.get("CI"):
-		return True
 	signature = frappe.request.headers.get("X-Razorpay-Signature")
 
 	settings = frappe.get_doc("Non Profit Settings")
@@ -225,16 +226,7 @@
 @frappe.whitelist(allow_guest=True)
 def trigger_razorpay_subscription(*args, **kwargs):
 	data = frappe.request.get_data(as_text=True)
-	try:
-		verify_signature(data)
-	except Exception as e:
-		log = frappe.log_error(e, "Membership Webhook Verification Error")
-		notify_failure(log)
-		return { "status": "Failed", "reason": e}
-
-	if isinstance(data, six.string_types):
-		data = json.loads(data)
-	data = frappe._dict(data)
+	data = process_request_data(data)
 
 	subscription = data.payload.get("subscription", {}).get("entity", {})
 	subscription = frappe._dict(subscription)
@@ -281,7 +273,7 @@
 		# Update membership values
 		member.subscription_start = datetime.fromtimestamp(subscription.start_at)
 		member.subscription_end = datetime.fromtimestamp(subscription.end_at)
-		member.subscription_activated = 1
+		member.subscription_status = "Active"
 		member.flags.ignore_mandatory = True
 		member.save()
 
@@ -294,9 +286,67 @@
 		message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id)
 		log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
 		notify_failure(log)
-		return { "status": "Failed", "reason": e}
+		return {"status": "Failed", "reason": e}
 
-	return { "status": "Success" }
+	return {"status": "Success"}
+
+
+@frappe.whitelist(allow_guest=True)
+def update_halted_razorpay_subscription(*args, **kwargs):
+	"""
+	When all retries have been exhausted, Razorpay moves the subscription to the halted state.
+	The customer has to manually retry the charge or change the card linked to the subscription,
+	for the subscription to move back to the active state.
+	"""
+	if frappe.request:
+		data = frappe.request.get_data(as_text=True)
+		data = process_request_data(data)
+	elif frappe.flags.in_test:
+		data = kwargs.get("data")
+		data = frappe._dict(data)
+	else:
+		return
+
+	if not data.event == "subscription.halted":
+		return
+
+	subscription = data.payload.get("subscription", {}).get("entity", {})
+	subscription = frappe._dict(subscription)
+
+	try:
+		member = get_member_based_on_subscription(subscription.id, customer_id=subscription.customer_id)
+		if not member:
+			frappe.throw(_("Member with Razorpay Subscription ID {0} not found").format(subscription.id))
+
+		member.subscription_status = "Halted"
+		member.flags.ignore_mandatory = True
+		member.save()
+
+		if subscription.get("notes"):
+			member = get_additional_notes(member, subscription)
+
+	except Exception as e:
+		message = "{0}\n\n{1}".format(e, frappe.get_traceback())
+		log = frappe.log_error(message, _("Error updating halted status for member {0}").format(member.name))
+		notify_failure(log)
+		return {"status": "Failed", "reason": e}
+
+	return {"status": "Success"}
+
+
+def process_request_data(data):
+	try:
+		verify_signature(data)
+	except Exception as e:
+		log = frappe.log_error(e, "Membership Webhook Verification Error")
+		notify_failure(log)
+		return {"status": "Failed", "reason": e}
+
+	if isinstance(data, six.string_types):
+		data = json.loads(data)
+	data = frappe._dict(data)
+
+	return data
 
 
 def get_company_for_memberships():
@@ -362,4 +412,4 @@
 			`tabMembership` SET `status` = 'Expired'
 		WHERE
 			`status` not in ('Cancelled') AND `to_date` < %s
-		""", (nowdate()))
\ No newline at end of file
+		""", (nowdate()))
diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py
index 31da792..0f5a9be 100644
--- a/erpnext/non_profit/doctype/membership/test_membership.py
+++ b/erpnext/non_profit/doctype/membership/test_membership.py
@@ -6,6 +6,7 @@
 import frappe
 import erpnext
 from erpnext.non_profit.doctype.member.member import create_member
+from erpnext.non_profit.doctype.membership.membership import update_halted_razorpay_subscription
 from frappe.utils import nowdate, add_months
 
 class TestMembership(unittest.TestCase):
@@ -13,11 +14,16 @@
 		plan = setup_membership()
 
 		# make test member
-		self.member_doc = create_member(frappe._dict({
-				'fullname': "_Test_Member",
-				'email': "_test_member_erpnext@example.com",
-				'plan_id': plan.name
-		}))
+		self.member_doc = create_member(
+			frappe._dict({
+				"fullname": "_Test_Member",
+				"email": "_test_member_erpnext@example.com",
+				"plan_id": plan.name,
+				"subscription_id": "sub_DEX6xcJ1HSW4CR",
+				"customer_id": "cust_C0WlbKhp3aLA7W",
+				"subscription_status": "Active"
+			})
+		)
 		self.member_doc.make_customer_and_link()
 		self.member = self.member_doc.name
 
@@ -51,6 +57,20 @@
 			"to_date": add_months(nowdate(), 3),
 		})
 
+	def test_halted_memberships(self):
+		make_membership(self.member, {
+			"from_date": add_months(nowdate(), 2),
+			"to_date": add_months(nowdate(), 3)
+		})
+
+		self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Active")
+		payload = get_subscription_payload()
+		update_halted_razorpay_subscription(data=payload)
+		self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Halted")
+
+	def tearDown(self):
+		frappe.db.rollback()
+
 def set_config(key, value):
 	frappe.db.set_value("Non Profit Settings", None, key, value)
 
@@ -115,4 +135,28 @@
 	else:
 		plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
 
-	return plan
\ No newline at end of file
+	return plan
+
+def get_subscription_payload():
+	return {
+		"entity": "event",
+		"account_id": "acc_BFQ7uQEaa7j2z7",
+		"event": "subscription.halted",
+		"contains": [
+			"subscription"
+		],
+		"payload": {
+			"subscription": {
+				"entity": {
+					"id": "sub_DEX6xcJ1HSW4CR",
+					"entity": "subscription",
+					"plan_id": "_rzpy_test_milythm",
+					"customer_id": "cust_C0WlbKhp3aLA7W",
+					"status": "halted",
+					"notes": {
+						"Important": "Notes for Internal Reference"
+					},
+				}
+			}
+		}
+	}
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index f63c7ed..2a83635 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -292,3 +292,4 @@
 erpnext.patches.v13_0.update_job_card_details
 erpnext.patches.v13_0.update_level_in_bom #1234sswef
 erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
+erpnext.patches.v13_0.update_subscription_status_in_memberships
diff --git a/erpnext/patches/v13_0/update_subscription_status_in_memberships.py b/erpnext/patches/v13_0/update_subscription_status_in_memberships.py
new file mode 100644
index 0000000..28e650e
--- /dev/null
+++ b/erpnext/patches/v13_0/update_subscription_status_in_memberships.py
@@ -0,0 +1,9 @@
+import frappe
+
+def execute():
+	if frappe.db.exists('DocType', 'Member'):
+		frappe.reload_doc('Non Profit', 'doctype', 'Member')
+
+		if frappe.db.has_column('Member', 'subscription_activated'):
+			frappe.db.sql('UPDATE `tabMember` SET subscription_status = "Active" WHERE subscription_activated = 1')
+			frappe.db.sql_ddl('ALTER table `tabMember` DROP COLUMN subscription_activated')
\ No newline at end of file
diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py
index 211b94a..d60b1a2 100644
--- a/erpnext/portal/product_configurator/utils.py
+++ b/erpnext/portal/product_configurator/utils.py
@@ -101,7 +101,7 @@
 	return html
 
 def set_item_group_filters(field_filters):
-	if 'item_group' in field_filters:
+	if field_filters is not None and 'item_group' in field_filters:
 		field_filters['item_group'] = [ig[0] for ig in get_child_groups(field_filters['item_group'])]
 
 
diff --git a/erpnext/public/js/utils/customer_quick_entry.js b/erpnext/public/js/utils/customer_quick_entry.js
index ebe6cd9..7bd21df 100644
--- a/erpnext/public/js/utils/customer_quick_entry.js
+++ b/erpnext/public/js/utils/customer_quick_entry.js
@@ -1,9 +1,9 @@
 frappe.provide('frappe.ui.form');
 
 frappe.ui.form.CustomerQuickEntryForm = frappe.ui.form.QuickEntryForm.extend({
-	init: function(doctype, after_insert) {
+	init: function(doctype, after_insert, init_callback, doc, force) {
+		this._super(doctype, after_insert, init_callback, doc, force);
 		this.skip_redirect_on_error = true;
-		this._super(doctype, after_insert);
 	},
 
 	render_dialog: function() {
diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
index ece9fb5..c3db27f 100644
--- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
+++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
@@ -12,10 +12,14 @@
 class TransactionDeletionRecord(Document):
 	def validate(self):
 		frappe.only_for('System Manager')
+		self.validate_doctypes_to_be_ignored()
+
+	def validate_doctypes_to_be_ignored(self):
 		doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
 		for doctype in self.doctypes_to_be_ignored:
 			if doctype.doctype_name not in doctypes_to_be_ignored_list:
-				frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it. "), title=_("Not Allowed"))
+				frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it."),
+					title=_("Not Allowed"))
 
 	def before_submit(self):
 		if not self.doctypes_to_be_ignored:
@@ -23,54 +27,9 @@
 
 		self.delete_bins()
 		self.delete_lead_addresses()
-		
-		company_obj = frappe.get_doc('Company', self.company)
-		# reset company values
-		company_obj.total_monthly_sales = 0
-		company_obj.sales_monthly_history = None
-		company_obj.save()
-		# Clear notification counts
+		self.reset_company_values()
 		clear_notifications()
-
-		singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name')
-		tables = frappe.get_all('DocType', filters = {'istable': 1}, pluck = 'name')
-		doctypes_to_be_ignored_list = singles
-		for doctype in self.doctypes_to_be_ignored:
-			doctypes_to_be_ignored_list.append(doctype.doctype_name)
-
-		docfields = frappe.get_all('DocField', 
-			filters = {
-				'fieldtype': 'Link', 
-				'options': 'Company',
-				'parent': ['not in', doctypes_to_be_ignored_list]},
-			fields=['parent', 'fieldname'])
-	
-		for docfield in docfields:
-			if docfield['parent'] != self.doctype:
-				no_of_docs = frappe.db.count(docfield['parent'], {
-							docfield['fieldname'] : self.company
-						})
-
-				if no_of_docs > 0:
-					self.delete_version_log(docfield['parent'], docfield['fieldname'])
-					self.delete_communications(docfield['parent'], docfield['fieldname'])
-
-					# populate DocTypes table
-					if docfield['parent'] not in tables:
-						self.append('doctypes', {
-							'doctype_name' : docfield['parent'],
-							'no_of_docs' : no_of_docs
-						})
-
-					# delete the docs linked with the specified company
-					frappe.db.delete(docfield['parent'], {
-						docfield['fieldname'] : self.company
-					})
-
-					naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname')
-					if naming_series:
-						if '#' in naming_series:
-							self.update_naming_series(naming_series, docfield['parent'])	
+		self.delete_company_transactions()
 
 	def populate_doctypes_to_be_ignored_table(self):		
 		doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
@@ -79,6 +38,111 @@
 						'doctype_name' : doctype
 					})
 
+	def delete_bins(self):
+		frappe.db.sql("""delete from tabBin where warehouse in
+				(select name from tabWarehouse where company=%s)""", self.company)
+
+	def delete_lead_addresses(self):
+		"""Delete addresses to which leads are linked"""
+		leads = frappe.get_all('Lead', filters={'company': self.company})
+		leads = ["'%s'" % row.get("name") for row in leads]
+		addresses = []
+		if leads:
+			addresses = frappe.db.sql_list("""select parent from `tabDynamic Link` where link_name
+				in ({leads})""".format(leads=",".join(leads)))
+
+			if addresses:
+				addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
+
+				frappe.db.sql("""delete from tabAddress where name in ({addresses}) and
+					name not in (select distinct dl1.parent from `tabDynamic Link` dl1
+					inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
+					and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses)))
+
+				frappe.db.sql("""delete from `tabDynamic Link` where link_doctype='Lead'
+					and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads)))
+
+			frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads)))
+
+	def reset_company_values(self):
+		company_obj = frappe.get_doc('Company', self.company)
+		company_obj.total_monthly_sales = 0
+		company_obj.sales_monthly_history = None
+		company_obj.save()
+
+	def delete_company_transactions(self):
+		doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list()
+		docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list)
+
+		tables = self.get_all_child_doctypes()	
+		for docfield in docfields:
+			if docfield['parent'] != self.doctype:
+				no_of_docs = self.get_number_of_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname'])
+
+				if no_of_docs > 0:
+					self.delete_version_log(docfield['parent'], docfield['fieldname'])
+					self.delete_communications(docfield['parent'], docfield['fieldname'])
+					self.populate_doctypes_table(tables, docfield['parent'], no_of_docs)
+
+					self.delete_child_tables(docfield['parent'], docfield['fieldname'])
+					self.delete_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname'])
+
+					naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname')
+					if naming_series:
+						if '#' in naming_series:
+							self.update_naming_series(naming_series, docfield['parent'])	
+
+	def get_doctypes_to_be_ignored_list(self):
+		singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name')
+		doctypes_to_be_ignored_list = singles
+		for doctype in self.doctypes_to_be_ignored:
+			doctypes_to_be_ignored_list.append(doctype.doctype_name)
+
+		return doctypes_to_be_ignored_list
+
+	def get_doctypes_with_company_field(self, doctypes_to_be_ignored_list):
+		docfields = frappe.get_all('DocField', 
+			filters = {
+				'fieldtype': 'Link', 
+				'options': 'Company',
+				'parent': ['not in', doctypes_to_be_ignored_list]},
+			fields=['parent', 'fieldname'])
+
+		return docfields
+
+	def get_all_child_doctypes(self):
+		return frappe.get_all('DocType', filters = {'istable': 1}, pluck = 'name')
+
+	def get_number_of_docs_linked_with_specified_company(self, doctype, company_fieldname):
+		return frappe.db.count(doctype, {company_fieldname : self.company})
+
+	def populate_doctypes_table(self, tables, doctype, no_of_docs):
+		if doctype not in tables:
+			self.append('doctypes', {
+				'doctype_name' : doctype,
+				'no_of_docs' : no_of_docs
+			})		
+
+	def delete_child_tables(self, doctype, company_fieldname):
+		parent_docs_to_be_deleted = frappe.get_all(doctype, {
+			company_fieldname : self.company
+		}, pluck = 'name')
+
+		child_tables = frappe.get_all('DocField', filters = {
+			'fieldtype': 'Table', 
+			'parent': doctype
+		}, pluck = 'options')
+
+		for table in child_tables:
+			frappe.db.delete(table, {
+				'parent': ['in', parent_docs_to_be_deleted]
+			})
+
+	def delete_docs_linked_with_specified_company(self, doctype, company_fieldname):
+		frappe.db.delete(doctype, {
+			company_fieldname : self.company
+		})
+
 	def update_naming_series(self, naming_series, doctype_name):
 		if '.' in naming_series:
 			prefix, hashes = naming_series.rsplit('.', 1)
@@ -107,32 +171,6 @@
 
 		frappe.delete_doc('Communication', communication_names, ignore_permissions=True)
 
-	def delete_bins(self):
-		frappe.db.sql("""delete from tabBin where warehouse in
-				(select name from tabWarehouse where company=%s)""", self.company)
-
-	def delete_lead_addresses(self):
-		"""Delete addresses to which leads are linked"""
-		leads = frappe.get_all('Lead', filters={'company': self.company})
-		leads = ["'%s'" % row.get("name") for row in leads]
-		addresses = []
-		if leads:
-			addresses = frappe.db.sql_list("""select parent from `tabDynamic Link` where link_name
-				in ({leads})""".format(leads=",".join(leads)))
-
-			if addresses:
-				addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
-
-				frappe.db.sql("""delete from tabAddress where name in ({addresses}) and
-					name not in (select distinct dl1.parent from `tabDynamic Link` dl1
-					inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
-					and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses)))
-
-				frappe.db.sql("""delete from `tabDynamic Link` where link_doctype='Lead'
-					and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads)))
-
-			frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads)))
-
 @frappe.whitelist()
 def get_doctypes_to_be_ignored():
 	doctypes_to_be_ignored_list = ['Account', 'Cost Center', 'Warehouse', 'Budget',
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index b55374b..264baea 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -100,10 +100,11 @@
 
 		frm.add_custom_button(__('Duplicate'), function() {
 			var new_item = frappe.model.copy_doc(frm.doc);
-			if(new_item.item_name===new_item.item_code) {
+			// Duplicate item could have different name, causing "copy paste" error.
+			if (new_item.item_name===new_item.item_code) {
 				new_item.item_name = null;
 			}
-			if(new_item.description===new_item.description) {
+			if (new_item.item_code===new_item.description || new_item.item_code===new_item.description) {
 				new_item.description = null;
 			}
 			frappe.set_route('Form', 'Item', new_item.name);
@@ -186,8 +187,6 @@
 	item_code: function(frm) {
 		if(!frm.doc.item_name)
 			frm.set_value("item_name", frm.doc.item_code);
-		if(!frm.doc.description)
-			frm.set_value("description", frm.doc.item_code);
 	},
 
 	is_stock_item: function(frm) {
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 4657700..cf52803 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -74,9 +74,8 @@
 
 	update_party_blanket_order(args, out)
 
-	if not doc or cint(doc.get('is_return')) == 0:
-		# get price list rate only if the invoice is not a credit or debit note
-		get_price_list_rate(args, item, out)
+	
+	get_price_list_rate(args, item, out)
 
 	if args.customer and cint(args.is_pos):
 		out.update(get_pos_profile_item_details(args.company, args, update_data=True))