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))