From 5d5f026a0d4ac2d088b722f254f7b58db27b8a4f Mon Sep 17 00:00:00 2001 From: Anupam K Date: Fri, 24 Jul 2020 02:32:45 +0530 Subject: [PATCH 01/42] fix: Target variance report signs --- .../item_group_wise_sales_target_variance.py | 20 +++++++++---------- ...ner_target_variance_based_on_item_group.js | 17 +++++++++++++++- ...son_target_variance_based_on_item_group.js | 17 +++++++++++++++- ...ory_target_variance_based_on_item_group.js | 17 +++++++++++++++- 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py index 857b9823e0..ae216ca5d6 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py @@ -63,13 +63,13 @@ def get_columns(filters, period_list, partner_doctype): "label": _(partner_doctype), "fieldtype": "Link", "options": partner_doctype, - "width": 100 + "width": 150 }, { "fieldname": "item_group", "label": _("Item Group"), "fieldtype": "Link", "options": "Item Group", - "width": 100 + "width": 150 }] for period in period_list: @@ -81,19 +81,19 @@ def get_columns(filters, period_list, partner_doctype): "label": _("Target ({})").format(period.label), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }, { "fieldname": period.key, "label": _("Achieved ({})").format(period.label), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }, { "fieldname": variance_key, "label": _("Variance ({})").format(period.label), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }]) columns.extend([{ @@ -101,19 +101,19 @@ def get_columns(filters, period_list, partner_doctype): "label": _("Total Target"), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }, { "fieldname": "total_achieved", "label": _("Total Achieved"), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }, { "fieldname": "total_variance", "label": _("Total Variance"), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }]) return columns @@ -154,10 +154,10 @@ def prepare_data(filters, sales_users_data, actual_data, date_field, period_list if (r.get(sales_field) == d.parent and r.item_group == d.item_group and period.from_date <= r.get(date_field) and r.get(date_field) <= period.to_date): details[p_key] += r.get(qty_or_amount_field, 0) - details[variance_key] = details.get(target_key) - details.get(p_key) + details[variance_key] = details.get(p_key) - details.get(target_key) details["total_achieved"] += details.get(p_key) - details["total_variance"] = details.get("total_target") - details.get("total_achieved") + details["total_variance"] = details.get("total_achieved") - details.get("total_target") return rows diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js index f99f68c524..38bb127e23 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js @@ -44,5 +44,20 @@ frappe.query_reports["Sales Partner Target Variance based on Item Group"] = { options: "Quantity\nAmount", default: "Quantity" }, - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes('variance')) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } } diff --git a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js index 9f6bfc41df..a8e2fad373 100644 --- a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js @@ -44,5 +44,20 @@ frappe.query_reports["Sales Person Target Variance Based On Item Group"] = { options: "Quantity\nAmount", default: "Quantity" }, - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes('variance')) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } } diff --git a/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js b/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js index dd9607ffbd..263391a7f7 100644 --- a/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js @@ -44,5 +44,20 @@ frappe.query_reports["Territory Target Variance Based On Item Group"] = { options: "Quantity\nAmount", default: "Quantity" }, - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes('variance')) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } } From 8718daa11e11c17ec64d550df661b3f3ac7c2eac Mon Sep 17 00:00:00 2001 From: Anupam K Date: Sat, 25 Jul 2020 23:56:11 +0530 Subject: [PATCH 02/42] Adding formatter in budget variance report --- .../budget_variance_report.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index 3ec4d306c3..30415d1e65 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -71,7 +71,22 @@ frappe.query_reports["Budget Variance Report"] = { fieldtype: "Check", default: 0, }, - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes('variance')) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } } erpnext.dimension_filters.forEach((dimension) => { From af1f46f2d97dbdcfbd1ee020ac79072edcc375fa Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 7 Aug 2020 19:44:20 +0530 Subject: [PATCH 03/42] fix: Add default billing address for purchase documents --- erpnext/public/js/controllers/transaction.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 4e50f3d7f6..862b6fbf9b 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -781,10 +781,23 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ else var date = this.frm.doc.transaction_date; if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && - in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)){ + in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) { erpnext.utils.get_shipping_address(this.frm, function(){ set_party_account(set_pricing); }) + + // Get default company billing address in Purchase Invoice, Order and Receipt + frappe.call({ + 'method': 'frappe.contacts.doctype.address.address.get_default_address', + 'args': { + 'doctype': 'Company', + 'name': this.frm.doc.company + }, + 'callback': function(r) { + me.frm.set_value('billing_address', r.message); + } + }); + } else { set_party_account(set_pricing); } From 069a54e5c38c10f8e97d16e4e279119adea69423 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 10 Aug 2020 16:01:01 +0530 Subject: [PATCH 04/42] fix: Cancellation of accounting transactions within closed accounting period --- erpnext/accounts/general_ledger.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index cf3deb828f..01d3903d28 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -45,8 +45,8 @@ def validate_accounting_period(gl_map): }, as_dict=1) if accounting_periods: - frappe.throw(_("You can't create accounting entries in the closed accounting period {0}") - .format(accounting_periods[0].name), ClosedAccountingPeriod) + frappe.throw(_("You cannot create or cancel any accounting entries with in the closed Accounting Period {0}") + .format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod) def process_gl_map(gl_map, merge_entries=True): if merge_entries: @@ -301,8 +301,9 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None, }) if gl_entries: - set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no']) + validate_accounting_period(gl_entries) check_freezing_date(gl_entries[0]["posting_date"], adv_adj) + set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no']) for entry in gl_entries: entry['name'] = None @@ -342,7 +343,7 @@ def set_as_cancel(voucher_type, voucher_no): """ Set is_cancelled=1 in all original gl entries for the voucher """ - frappe.db.sql("""update `tabGL Entry` set is_cancelled = 1, + frappe.db.sql("""UPDATE `tabGL Entry` SET is_cancelled = 1, modified=%s, modified_by=%s where voucher_type=%s and voucher_no=%s and is_cancelled = 0""", (now(), frappe.session.user, voucher_type, voucher_no)) From b3bd780d46cd9d9d48033d9bb8b074a25942e598 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Thu, 13 Aug 2020 17:07:15 +0530 Subject: [PATCH 05/42] fix: codasy dix --- .../report/budget_variance_report/budget_variance_report.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index 30415d1e65..f547ca619b 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -74,9 +74,9 @@ frappe.query_reports["Budget Variance Report"] = { ], "formatter": function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - + if (column.fieldname.includes('variance')) { - + if (data[column.fieldname] < 0) { value = "" + value + ""; } From a16b24d50a7580862e1610a95838de2545bd1a58 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 11:06:42 +0530 Subject: [PATCH 06/42] feat: add enable or disable invoicing and print format field --- .../membership_settings.js | 9 +++++++ .../membership_settings.json | 26 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index 8c0e3a4fa7..bbfece31d4 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -10,6 +10,15 @@ frappe.ui.form.on("Membership Settings", { }) }); } + + frm.set_query('print_format', function(doc) { + return { + filters: { + "doc_type": "Sales Invoice" + } + }; + }); + frm.trigger("add_generate_button"); }, diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 52b9d01088..29013fafdc 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -9,7 +9,10 @@ "razorpay_settings_section", "billing_cycle", "billing_frequency", - "webhook_secret" + "webhook_secret", + "column_break_6", + "enable_auto_invoicing", + "print_format" ], "fields": [ { @@ -41,11 +44,30 @@ "fieldtype": "Password", "label": "Webhook Secret", "read_only": 1 + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "enable_auto_invoicing", + "fieldtype": "Check", + "label": "Enable Auto Invoicing" + }, + { + "depends_on": "eval:doc.enable_auto_invoicing", + "fieldname": "print_format", + "fieldtype": "Link", + "label": "Print Format", + "mandatory_depends_on": "eval:doc.enable_auto_invoicing", + "options": "Print Format" } ], + "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-05-22 12:38:27.103759", + "modified": "2020-07-28 11:01:40.125896", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 3a1868dae813a41ee9e124ae05f20374214c35db Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 11:07:35 +0530 Subject: [PATCH 07/42] feat: add item field --- .../doctype/membership_type/membership_type.js | 6 +++++- .../doctype/membership_type/membership_type.json | 12 ++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js index 226981dc78..43311a2c96 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.js +++ b/erpnext/non_profit/doctype/membership_type/membership_type.js @@ -5,6 +5,10 @@ frappe.ui.form.on('Membership Type', { refresh: function(frm) { frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false); - }) + }); + + frappe.db.get_single_value("Membership Settings", "enable_auto_invoicing").then(val => { + if (val) frm.set_df_property('linked_item', 'hidden', false); + }); } }); diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.json b/erpnext/non_profit/doctype/membership_type/membership_type.json index 319078fd6c..a163568bb9 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.json +++ b/erpnext/non_profit/doctype/membership_type/membership_type.json @@ -8,7 +8,8 @@ "field_order": [ "membership_type", "amount", - "razorpay_plan_id" + "razorpay_plan_id", + "linked_item" ], "fields": [ { @@ -33,10 +34,17 @@ "hidden": 1, "label": "Razorpay Plan ID", "unique": 1 + }, + { + "fieldname": "linked_item", + "fieldtype": "Link", + "label": "Linked Item", + "options": "Item" } ], + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-03-30 12:54:07.850857", + "modified": "2020-07-28 10:57:50.821375", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Type", From 96dc67c35edf360721c83ac4ae87e8d76d3851f0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 11:43:54 +0530 Subject: [PATCH 08/42] feat: add copy button and docs link --- .../membership_settings/membership_settings.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index bbfece31d4..f5e0274c3a 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -19,7 +19,12 @@ frappe.ui.form.on("Membership Settings", { }; }); + let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership"; + + frm.set_intro(__("You can learn more about memberships in the manual. ") + `${__('ERPNext Docs')}`, true); + frm.trigger("add_generate_button"); + frm.trigger("add_copy_buttonn"); }, add_generate_button: function(frm) { @@ -36,4 +41,12 @@ frappe.ui.form.on("Membership Settings", { }); }); }, + + add_copy_buttonn: function(frm) { + if (frm.doc.webhook_secret) { + frm.add_custom_button(__("Copy Webhook URL"), () => { + frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.membership.membership.trigger_razorpay_subscription`); + }); + } + } }); From 811ac909f93d543cd537241fa8522393281c9bcf Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 12:27:37 +0530 Subject: [PATCH 09/42] feat: add auto invoice creation fields and controllers --- .../doctype/membership/membership.py | 29 ++++++++++ .../membership_settings.js | 10 ++++ .../membership_settings.json | 57 +++++++++++++++++-- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 729e111e57..eea18393f1 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -57,6 +57,35 @@ class Membership(Document): self.load_from_db() self.db_set('paid', 1) + def generate_and_send_invoice(self): + if not self.paid: + frappe.throw(_("The payment for this membership is not paid. To generate invoice mark the paid check")) + + member = frappe.get_doc("Member", self.member) + plan = frappe.get_doc("Membership Type", self.membership_type) + settings = frappe.get_doc("Membership Settings") + + invoice = make_invoice(self, member, plan, settings) + +def make_invoice(membership, member, plan, settings): + invoice = frappe.get_doc({ + 'doctype': 'Sales Invoice', + 'customer': member.customer, + 'debit_to': settings.debit_account, + 'currency': membership.currency, + 'is_pos': 0, + 'items': [ + { + 'item_code': plan.linked_item, + 'rate': membership.amount, + 'qty': 1 + } + ] + }) + + invoice.insert(ignore_permissions=True) + invoice.submit() + def get_member_based_on_subscription(subscription_id, email): members = frappe.get_all("Member", filters={ 'subscription_id': subscription_id, diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index f5e0274c3a..02ef0292ea 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -19,6 +19,16 @@ frappe.ui.form.on("Membership Settings", { }; }); + frm.set_query('debit_account', function(doc) { + return { + filters: { + 'account_type': 'Receivable', + 'is_group': 0, + 'company': frm.doc.company + } + }; + }); + let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership"; frm.set_intro(__("You can learn more about memberships in the manual. ") + `${__('ERPNext Docs')}`, true); diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 29013fafdc..37bea49826 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -11,8 +11,15 @@ "billing_frequency", "webhook_secret", "column_break_6", + "print_format", + "company", + "debit_account", + "column_break_9", "enable_auto_invoicing", - "print_format" + "send_invoice", + "section_break_10", + "new_member", + "renewal" ], "fields": [ { @@ -47,7 +54,8 @@ }, { "fieldname": "column_break_6", - "fieldtype": "Column Break" + "fieldtype": "Section Break", + "label": "Invoicing" }, { "default": "0", @@ -56,18 +64,57 @@ "label": "Enable Auto Invoicing" }, { - "depends_on": "eval:doc.enable_auto_invoicing", "fieldname": "print_format", "fieldtype": "Link", "label": "Print Format", - "mandatory_depends_on": "eval:doc.enable_auto_invoicing", "options": "Print Format" + }, + { + "fieldname": "new_member", + "fieldtype": "Text Editor", + "label": "Message for New Member", + "mandatory_depends_on": "eval:doc.send_invoice" + }, + { + "fieldname": "renewal", + "fieldtype": "Text Editor", + "label": "Message for Renewal", + "mandatory_depends_on": "eval:doc.send_invoice" + }, + { + "depends_on": "eval:doc.company", + "fieldname": "debit_account", + "fieldtype": "Link", + "label": "Debit Account", + "options": "Account" + }, + { + "depends_on": "eval:doc.send_invoice", + "fieldname": "section_break_10", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "default": "0", + "fieldname": "send_invoice", + "fieldtype": "Check", + "label": "Send Email with Invoice" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-07-28 11:01:40.125896", + "modified": "2020-07-28 12:18:35.289893", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 15340e0c7f7c21da75403a54b66b502650f04a07 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 12:56:14 +0530 Subject: [PATCH 10/42] feat: send invoice via email --- .../doctype/membership/membership.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index eea18393f1..82b3145a07 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -67,6 +67,25 @@ class Membership(Document): invoice = make_invoice(self, member, plan, settings) + if invoice and settings.send_invoice and self.membership_status in ["New", "Current"]: + print("Sending") + message = settings.new_message if self.membership_status == "New" else settings.renewal + email = member.email_id if member.email_id else member.email + + email_args = { + "recipients": [email], + "message": message, + "subject": _('Here is your invoice'), + "attachments": [frappe.attach_print("Sales Invoice", invoice.name, print_format=settings.print_format)], + "reference_doctype": self.doctype, + "reference_name": self.name + } + if not frappe.flags.in_test: + frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) + else: + frappe.sendmail(**email_args) + + def make_invoice(membership, member, plan, settings): invoice = frappe.get_doc({ 'doctype': 'Sales Invoice', @@ -86,6 +105,8 @@ def make_invoice(membership, member, plan, settings): invoice.insert(ignore_permissions=True) invoice.submit() + return invoice + def get_member_based_on_subscription(subscription_id, email): members = frappe.get_all("Member", filters={ 'subscription_id': subscription_id, From 62e344188a2f82f70eca1910081dc6218b1655c0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 13:46:50 +0530 Subject: [PATCH 11/42] feat: update settings and link filters --- .../membership_settings.js | 10 ++- .../membership_settings.json | 83 ++++++++++++------- 2 files changed, 60 insertions(+), 33 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index 02ef0292ea..1d894027b0 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -11,7 +11,7 @@ frappe.ui.form.on("Membership Settings", { }); } - frm.set_query('print_format', function(doc) { + frm.set_query('inv_print_format', function(doc) { return { filters: { "doc_type": "Sales Invoice" @@ -19,6 +19,14 @@ frappe.ui.form.on("Membership Settings", { }; }); + frm.set_query('membership_print_format', function(doc) { + return { + filters: { + "doc_type": "Membership" + } + }; + }); + frm.set_query('debit_account', function(doc) { return { filters: { diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 37bea49826..7eeb7fbde2 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -11,15 +11,14 @@ "billing_frequency", "webhook_secret", "column_break_6", - "print_format", + "enable_auto_invoicing", "company", "debit_account", "column_break_9", - "enable_auto_invoicing", + "send_email", "send_invoice", - "section_break_10", - "new_member", - "renewal" + "membership_print_format", + "inv_print_format" ], "fields": [ { @@ -61,60 +60,63 @@ "default": "0", "fieldname": "enable_auto_invoicing", "fieldtype": "Check", - "label": "Enable Auto Invoicing" - }, - { - "fieldname": "print_format", - "fieldtype": "Link", - "label": "Print Format", - "options": "Print Format" - }, - { - "fieldname": "new_member", - "fieldtype": "Text Editor", - "label": "Message for New Member", + "label": "Enable Auto Invoicing", "mandatory_depends_on": "eval:doc.send_invoice" }, { - "fieldname": "renewal", - "fieldtype": "Text Editor", - "label": "Message for Renewal", - "mandatory_depends_on": "eval:doc.send_invoice" - }, - { - "depends_on": "eval:doc.company", + "depends_on": "eval:doc.enable_auto_invoicing", "fieldname": "debit_account", "fieldtype": "Link", "label": "Debit Account", + "mandatory_depends_on": "eval:doc.enable_auto_invoicing", "options": "Account" }, - { - "depends_on": "eval:doc.send_invoice", - "fieldname": "section_break_10", - "fieldtype": "Section Break", - "hide_border": 1 - }, { "fieldname": "column_break_9", "fieldtype": "Column Break" }, { + "depends_on": "eval:doc.enable_auto_invoicing", "fieldname": "company", "fieldtype": "Link", "label": "Company", + "mandatory_depends_on": "eval:doc.enable_auto_invoicing", "options": "Company" }, { "default": "0", + "depends_on": "eval:doc.enable_auto_invoicing && doc.send_email", "fieldname": "send_invoice", "fieldtype": "Check", - "label": "Send Email with Invoice" + "label": "Send Invoice with Email" + }, + { + "default": "0", + "fieldname": "send_email", + "fieldtype": "Check", + "label": "Send Acknowledge Email" + }, + { + "depends_on": "eval: doc.send_invoice", + "fieldname": "inv_print_format", + "fieldtype": "Link", + "label": "Invoice Print Format", + "mandatory_depends_on": "eval: doc.send_invoice", + "options": "Print Format" + }, + { + "depends_on": "eval:doc.send_email", + "fieldname": "membership_print_format", + "fieldtype": "Link", + "label": "Membership Print Format", + "mandatory_depends_on": "eval:doc.send_email", + "options": "Print Format" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-07-28 12:18:35.289893", + "modified": "2020-07-31 13:45:28.868235", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", @@ -129,6 +131,23 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Non Profit Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "role": "Non Profit Member", + "share": 1 } ], "quick_entry": 1, From 1678eb9b506af6a3f4a1e20c1e91136c17493a13 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 14:00:22 +0530 Subject: [PATCH 12/42] feat: drop payload column --- .../doctype/membership/membership.json | 16 +++++++--------- erpnext/patches.txt | 1 + .../v13_0/drop_razorpay_payload_column.py | 7 +++++++ 3 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 erpnext/patches/v13_0/drop_razorpay_payload_column.py diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json index 238f4c31fd..95bb3a5d84 100644 --- a/erpnext/non_profit/doctype/membership/membership.json +++ b/erpnext/non_profit/doctype/membership/membership.json @@ -19,10 +19,10 @@ "paid", "currency", "amount", + "invoice", "razorpay_details_section", "subscription_id", - "payment_id", - "webhook_payload" + "payment_id" ], "fields": [ { @@ -118,17 +118,15 @@ "read_only": 1 }, { - "fieldname": "webhook_payload", - "fieldtype": "Code", - "hidden": 1, - "label": "Webhook Payload", - "options": "JSON", - "read_only": 1 + "fieldname": "invoice", + "fieldtype": "Link", + "label": "Invoice", + "options": "Sales Invoice" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-07-27 14:28:11.532696", + "modified": "2020-07-31 13:57:02.328995", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 361fe8352a..17b46ab649 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -721,3 +721,4 @@ erpnext.patches.v13_0.healthcare_lab_module_rename_doctypes erpnext.patches.v13_0.stock_entry_enhancements erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.rename_lost_reason_detail +erpnext.patches.v13_0.drop_razorpay_payload_column diff --git a/erpnext/patches/v13_0/drop_razorpay_payload_column.py b/erpnext/patches/v13_0/drop_razorpay_payload_column.py new file mode 100644 index 0000000000..62f0373008 --- /dev/null +++ b/erpnext/patches/v13_0/drop_razorpay_payload_column.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + if frappe.db.exists("DocType", "Membership Settings"): + if 'webhook_payload' in frappe.db.get_table_columns("Membership Settings"): + frappe.db.sql("alter table `tabMembership Settings` drop column webhook_payload") \ No newline at end of file From c8d9e7f77bb82284eeb322290f28273a6c2d46e6 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 14:31:24 +0530 Subject: [PATCH 13/42] feat: add message field --- .../membership_settings.json | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 7eeb7fbde2..2452a763ce 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -18,7 +18,9 @@ "send_email", "send_invoice", "membership_print_format", - "inv_print_format" + "inv_print_format", + "section_break_15", + "message" ], "fields": [ { @@ -111,12 +113,24 @@ "label": "Membership Print Format", "mandatory_depends_on": "eval:doc.send_email", "options": "Print Format" + }, + { + "depends_on": "eval:doc.send_email", + "fieldname": "section_break_15", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "depends_on": "eval:doc.send_email", + "fieldname": "message", + "fieldtype": "Text Editor", + "label": "Message" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-07-31 13:45:28.868235", + "modified": "2020-07-31 14:30:15.701767", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 8e0314e7f684f0ae52963d4daf5a692ccd0308fa Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 14:31:39 +0530 Subject: [PATCH 14/42] feat: separate invoice generation and email --- .../doctype/membership/membership.py | 69 ++++++++++++++----- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 82b3145a07..c960daa1d3 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -57,34 +57,65 @@ class Membership(Document): self.load_from_db() self.db_set('paid', 1) - def generate_and_send_invoice(self): - if not self.paid: - frappe.throw(_("The payment for this membership is not paid. To generate invoice mark the paid check")) + def generate_invoice(self, save=True): + if not (self.paid or self.currency or self.amount): + frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details")) + + if self.invoice: + frappe.throw(_("An invoice is already linked to this document")) member = frappe.get_doc("Member", self.member) plan = frappe.get_doc("Membership Type", self.membership_type) settings = frappe.get_doc("Membership Settings") + attachments = [] + + if not member.customer: + frappe.throw(_("No customer linked to member {}", [member.name])) + + if not settings.debit_account: + frappe.throw(_("You need to set Debit Account in Membership Settings")) + + if not settings.company: + frappe.throw(_("You need to set Default Company for invoicing in Membership Settings")) invoice = make_invoice(self, member, plan, settings) + self.invoice = invoice.name - if invoice and settings.send_invoice and self.membership_status in ["New", "Current"]: - print("Sending") - message = settings.new_message if self.membership_status == "New" else settings.renewal - email = member.email_id if member.email_id else member.email + if save: + self.save() - email_args = { - "recipients": [email], - "message": message, - "subject": _('Here is your invoice'), - "attachments": [frappe.attach_print("Sales Invoice", invoice.name, print_format=settings.print_format)], - "reference_doctype": self.doctype, - "reference_name": self.name - } - if not frappe.flags.in_test: - frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) - else: - frappe.sendmail(**email_args) + return invoice + def send_acknowlement(self): + settings = frappe.get_doc("Membership Settings") + if not settings.send_email: + frappe.throw(_("You need to enable Send Acknowledge Email in Membership Settings")) + + member = frappe.get_doc("Member", self.member) + plan = frappe.get_doc("Membership Type", self.membership_type) + email = member.email_id if member.email_id else member.email + attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)] + + if self.invoice and settings.send_invoice: + attachments.append(frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format)) + + email_args = { + "recipients": [email], + "message": settings.message, + "subject": _('Here is your invoice'), + "attachments": [frappe.attach_print("Sales Invoice", invoice.name, print_format=settings.inv_print_format)], + "reference_doctype": self.doctype, + "reference_name": self.name + } + + if not frappe.flags.in_test: + frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) + else: + frappe.sendmail(**email_args) + + def generate_and_send_invoice(self): + invoice = self.generate_invoice(False) + self.send_acknowlement() def make_invoice(membership, member, plan, settings): invoice = frappe.get_doc({ From 3a67a78ece8ad5a102e0d9623e3666c607532df4 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 14:54:06 +0530 Subject: [PATCH 15/42] feat: add custom buttons for invoice and email --- .../non_profit/doctype/membership/membership.js | 16 ++++++++++++++++ .../non_profit/doctype/membership/membership.py | 1 - 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js index 554549a0bd..8408a6a1f0 100644 --- a/erpnext/non_profit/doctype/membership/membership.js +++ b/erpnext/non_profit/doctype/membership/membership.js @@ -8,6 +8,22 @@ frappe.ui.form.on('Membership', { }) }, + refresh: function(frm) { + !frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => { + frm.call("generate_invoice", { + save: true + }).then(() => { + frm.reload_doc(); + }); + }); + + frm.add_custom_button("Send Acknowledgement", () => { + frm.call("send_acknowlement").then(() => { + frm.reload_doc(); + }); + }); + }, + onload: function(frm) { frm.add_fetch('membership_type', 'amount', 'amount'); } diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index c960daa1d3..8d1e44da5d 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -67,7 +67,6 @@ class Membership(Document): member = frappe.get_doc("Member", self.member) plan = frappe.get_doc("Membership Type", self.membership_type) settings = frappe.get_doc("Membership Settings") - attachments = [] if not member.customer: frappe.throw(_("No customer linked to member {}", [member.name])) From 733bde31c9e3de279a77de361c4392fa649777ec Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 15:06:51 +0530 Subject: [PATCH 16/42] fix: doctype name in patch --- erpnext/patches/v13_0/drop_razorpay_payload_column.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v13_0/drop_razorpay_payload_column.py b/erpnext/patches/v13_0/drop_razorpay_payload_column.py index 62f0373008..8980fd0039 100644 --- a/erpnext/patches/v13_0/drop_razorpay_payload_column.py +++ b/erpnext/patches/v13_0/drop_razorpay_payload_column.py @@ -2,6 +2,6 @@ from __future__ import unicode_literals import frappe def execute(): - if frappe.db.exists("DocType", "Membership Settings"): - if 'webhook_payload' in frappe.db.get_table_columns("Membership Settings"): - frappe.db.sql("alter table `tabMembership Settings` drop column webhook_payload") \ No newline at end of file + if frappe.db.exists("DocType", "Membership"): + if 'webhook_payload' in frappe.db.get_table_columns("Membership"): + frappe.db.sql("alter table `tabMembership` drop column webhook_payload") \ No newline at end of file From 6fbe9b54041b1325284774b60e9e475f8b2208a3 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sat, 1 Aug 2020 13:58:21 +0530 Subject: [PATCH 17/42] fix: remove payload --- erpnext/non_profit/doctype/membership/membership.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 8d1e44da5d..bfc2661a8f 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -200,7 +200,6 @@ def trigger_razorpay_subscription(*args, **kwargs): "currency": "INR", "paid": 1, "payment_id": payment.id, - "webhook_payload": data_json, "from_date": datetime.fromtimestamp(subscription.current_start), "to_date": datetime.fromtimestamp(subscription.current_end), "amount": payment.amount / 100 # Convert to rupees from paise From 87ddec069e2da7d624554696eeeba7f9d7db3e56 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sat, 1 Aug 2020 14:01:41 +0530 Subject: [PATCH 18/42] refactor: try block across the function --- erpnext/non_profit/doctype/membership/membership.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index bfc2661a8f..a3a1b5b275 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -157,9 +157,7 @@ def verify_signature(data): controller.verify_signature(data, signature, key) - -@frappe.whitelist(allow_guest=True) -def trigger_razorpay_subscription(*args, **kwargs): +def make_membership_entry(*args, **kwargs): data = frappe.request.get_data(as_text=True) try: verify_signature(data) @@ -218,6 +216,14 @@ def trigger_razorpay_subscription(*args, **kwargs): return { status: 'Success' } +@frappe.whitelist(allow_guest=True) +def trigger_razorpay_subscription(*args, **kwargs): + try: + return make_membership_entry(*args, **kwargs) + except Exception as e: + log = frappe.log_error(e, "Webhook Failed") + return { status: 'Failed' } + def notify_failure(log): try: From d7139bbd437f8dc4f58e64ae36e6733539b66952 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sun, 2 Aug 2020 12:03:01 +0530 Subject: [PATCH 19/42] fix: type error --- erpnext/non_profit/doctype/membership/membership.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index a3a1b5b275..dc243d8c75 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -182,10 +182,10 @@ def make_membership_entry(*args, **kwargs): except Exception as e: error_log = frappe.log_error(frappe.get_traceback() + '\n' + data_json , _("Membership Webhook Failed")) notify_failure(error_log) - return { status: 'Failed' } + return { 'status': 'Failed' } if not member: - return { status: 'Failed' } + return { 'status': 'Failed' } try: if data.event == "subscription.activated": member.customer_id = payment.customer_id @@ -212,9 +212,9 @@ def make_membership_entry(*args, **kwargs): except Exception as e: log = frappe.log_error(e, "Error creating membership entry") notify_failure(log) - return { status: 'Failed' } + return { 'status': 'Failed' } - return { status: 'Success' } + return { 'status': 'Success' } @frappe.whitelist(allow_guest=True) def trigger_razorpay_subscription(*args, **kwargs): @@ -222,7 +222,7 @@ def trigger_razorpay_subscription(*args, **kwargs): return make_membership_entry(*args, **kwargs) except Exception as e: log = frappe.log_error(e, "Webhook Failed") - return { status: 'Failed' } + return { 'status': 'Failed' } def notify_failure(log): From 98bcedd09c510c52fe823342e849bc707d9b20e5 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sun, 2 Aug 2020 14:50:27 +0530 Subject: [PATCH 20/42] feat: create member if not already exists --- erpnext/non_profit/doctype/member/member.py | 2 +- .../doctype/membership/membership.py | 85 +++++++++++-------- .../membership_type/membership_type.py | 2 +- 3 files changed, 50 insertions(+), 39 deletions(-) diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index c52082ca23..6c2da07bb7 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -78,7 +78,7 @@ def create_member(user_details): member.update({ "member_name": user_details.fullname, "email_id": user_details.email, - "pan_number": user_details.pan, + "pan_number": user_details.pan or None, "membership_type": user_details.plan_id, "customer": create_customer(user_details) }) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index dc243d8c75..fc89396fa8 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -10,6 +10,7 @@ from datetime import datetime from frappe.model.document import Document from frappe.email import sendmail_to_system_managers from frappe.utils import add_days, add_years, nowdate, getdate, add_months, get_link_to_form +from erpnext.non_profit.doctype.member.member import create_member from frappe import _ import erpnext @@ -142,6 +143,7 @@ def get_member_based_on_subscription(subscription_id, email): 'subscription_id': subscription_id, 'email_id': email }, order_by="creation desc") + try: return frappe.get_doc("Member", members[0]['name']) except: @@ -157,14 +159,15 @@ def verify_signature(data): controller.verify_signature(data, signature, key) -def make_membership_entry(*args, **kwargs): +@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: - signature = frappe.request.headers.get('X-Razorpay-Signature') - log = "{0} \n\n {1} \n\n {2} \n\n {3}".format(e, frappe.get_traceback(), signature, data) - frappe.log_error(e, "Webhook Verification Error") + log = frappe.log_error(e, "Webhook Verification Error") + notify_failure(log) + return { 'status': 'Failed', 'reason': e} if isinstance(data, six.string_types): data = json.loads(data) @@ -177,34 +180,42 @@ def make_membership_entry(*args, **kwargs): payment = frappe._dict(payment) try: - data_json = json.dumps(data, indent=4, sort_keys=True) + if not data.event == "subscription.charged": + return + member = get_member_based_on_subscription(subscription.id, payment.email) - except Exception as e: - error_log = frappe.log_error(frappe.get_traceback() + '\n' + data_json , _("Membership Webhook Failed")) - notify_failure(error_log) - return { 'status': 'Failed' } + if not member: + member = create_member(frappe._dict({ + 'fullname': payment.email, + 'email': payment.email, + 'plan_id': get_plan_from_razorpay_id(subscription.plan_id) + })) - if not member: - return { 'status': 'Failed' } - try: - if data.event == "subscription.activated": + member.subscription_id = subscription.id member.customer_id = payment.customer_id - elif data.event == "subscription.charged": - membership = frappe.new_doc("Membership") - membership.update({ - "member": member.name, - "membership_status": "Current", - "membership_type": member.membership_type, - "currency": "INR", - "paid": 1, - "payment_id": payment.id, - "from_date": datetime.fromtimestamp(subscription.current_start), - "to_date": datetime.fromtimestamp(subscription.current_end), - "amount": payment.amount / 100 # Convert to rupees from paise - }) - membership.insert(ignore_permissions=True) + if subscription.notes and type(subscription.notes) == dict: + notes = '\n'.join("{}: {}".format(k, v) for k, v in subscription.notes.items()) + member.add_comment("Comment", notes) + elif subscription.notes and type(subscription.notes) == str: + member.add_comment("Comment", subscription.notes) - # Update these values anyway + + # Update Membership + membership = frappe.new_doc("Membership") + membership.update({ + "member": member.name, + "membership_status": "Current", + "membership_type": member.membership_type, + "currency": "INR", + "paid": 1, + "payment_id": payment.id, + "from_date": datetime.fromtimestamp(subscription.current_start), + "to_date": datetime.fromtimestamp(subscription.current_end), + "amount": payment.amount / 100 # Convert to rupees from paise + }) + membership.insert(ignore_permissions=True) + + # Update membership values member.subscription_start = datetime.fromtimestamp(subscription.start_at) member.subscription_end = datetime.fromtimestamp(subscription.end_at) member.subscription_activated = 1 @@ -212,18 +223,10 @@ def make_membership_entry(*args, **kwargs): except Exception as e: log = frappe.log_error(e, "Error creating membership entry") notify_failure(log) - return { 'status': 'Failed' } + return { 'status': 'Failed', 'reason': e} return { 'status': 'Success' } -@frappe.whitelist(allow_guest=True) -def trigger_razorpay_subscription(*args, **kwargs): - try: - return make_membership_entry(*args, **kwargs) - except Exception as e: - log = frappe.log_error(e, "Webhook Failed") - return { 'status': 'Failed' } - def notify_failure(log): try: @@ -237,3 +240,11 @@ Administrator""".format(get_link_to_form("Error Log", log.name)) sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content) except: pass + +def get_plan_from_razorpay_id(plan_id): + plan = frappe.get_all("Membership Type", filters={'razorpay_plan_id': plan_id}, order_by="creation desc") + + try: + return plan[0]['name'] + except: + return None \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py index ed6b549600..7fa98a9481 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ b/erpnext/non_profit/doctype/membership_type/membership_type.py @@ -6,4 +6,4 @@ from __future__ import unicode_literals from frappe.model.document import Document class MembershipType(Document): - pass + pass \ No newline at end of file From e4c58c37c84c65f3f6eaac8ad4d2287e760df8a0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 3 Aug 2020 14:56:01 +0530 Subject: [PATCH 21/42] feat: allow member creation via API --- erpnext/non_profit/doctype/member/member.py | 42 ++++++++++++++++--- .../membership_type/membership_type.json | 6 +-- .../membership_type/membership_type.py | 6 ++- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 6c2da07bb7..797736a3db 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -9,6 +9,7 @@ from frappe.model.document import Document from frappe.contacts.address_and_contact import load_address_and_contact from frappe.utils import cint from frappe.integrations.utils import get_payment_gateway_controller +from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type class Member(Document): def onload(self): @@ -74,19 +75,23 @@ def get_or_create_member(user_details): return create_member(user_details) def create_member(user_details): + user_details = frappe._dict(user_details) member = frappe.new_doc("Member") member.update({ "member_name": user_details.fullname, "email_id": user_details.email, "pan_number": user_details.pan or None, "membership_type": user_details.plan_id, - "customer": create_customer(user_details) + "subscription_id": user_details.subscription_id or None }) member.insert(ignore_permissions=True) + member.customer = create_customer(user_details, member.name) + member.save(ignore_permissions=True) + return member -def create_customer(user_details): +def create_customer(user_details, member=None): customer = frappe.new_doc("Customer") customer.customer_name = user_details.fullname customer.customer_type = "Individual" @@ -107,7 +112,13 @@ def create_customer(user_details): "link_name": customer.name }) - contact.save() + if member: + contact.append("links", { + "link_doctype": "Member", + "link_name": member + }) + + contact.save(ignore_permissions=True) except frappe.DuplicateEntryError: return customer.name @@ -139,12 +150,31 @@ def create_member_subscription_order(user_details): user_details = frappe._dict(user_details) member = get_or_create_member(user_details) - if not member: - member = create_member(user_details) subscription = member.setup_subscription() member.subscription_id = subscription.get('subscription_id') member.save(ignore_permissions=True) - return subscription \ No newline at end of file + return subscription + +@frappe.whitelist(allow_guest=True) +def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, mobile=None): + plan = get_membership_type(rzpay_plan_id) + if not plan: + raise frappe.DoesNotExistError + + member = frappe.db.exists("Member", {'email': email, 'subscription_id': subscription_id }) + if member: + return member + else: + member = create_member(dict( + fullname=fullname, + email=email, + plan_id=plan, + subscription_id=subscription_id, + pan=pan, + mobile=mobile + )) + + return member.name \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.json b/erpnext/non_profit/doctype/membership_type/membership_type.json index a163568bb9..6ce1ecde12 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.json +++ b/erpnext/non_profit/doctype/membership_type/membership_type.json @@ -39,12 +39,12 @@ "fieldname": "linked_item", "fieldtype": "Link", "label": "Linked Item", - "options": "Item" + "options": "Item", + "unique": 1 } ], - "index_web_pages_for_search": 1, "links": [], - "modified": "2020-07-28 10:57:50.821375", + "modified": "2020-08-05 15:21:43.595745", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Type", diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py index 7fa98a9481..b95b04316f 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ b/erpnext/non_profit/doctype/membership_type/membership_type.py @@ -4,6 +4,10 @@ from __future__ import unicode_literals from frappe.model.document import Document +import frappe class MembershipType(Document): - pass \ No newline at end of file + pass + +def get_membership_type(razorpay_id): + return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id}) \ No newline at end of file From b4d3666e69f8a469d358ecb2cd082c45bce9f065 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 5 Aug 2020 17:33:18 +0530 Subject: [PATCH 22/42] feat: show send acknowledgement button if enabled in settings --- erpnext/non_profit/doctype/membership/membership.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js index 8408a6a1f0..ee8a8c0a7b 100644 --- a/erpnext/non_profit/doctype/membership/membership.js +++ b/erpnext/non_profit/doctype/membership/membership.js @@ -17,11 +17,13 @@ frappe.ui.form.on('Membership', { }); }); - frm.add_custom_button("Send Acknowledgement", () => { - frm.call("send_acknowlement").then(() => { - frm.reload_doc(); + frappe.db.get_single_value("Membership Settings", "send_email").then(val => { + if (val) frm.add_custom_button("Send Acknowledgement", () => { + frm.call("send_acknowlement").then(() => { + frm.reload_doc(); + }); }); - }); + }) }, onload: function(frm) { From 6b68eaad1ec1eb770e3cb2ae0b73fe4f43d1b219 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 5 Aug 2020 17:33:33 +0530 Subject: [PATCH 23/42] feat: use email template for membership acknowledgement --- .../doctype/membership/membership.py | 9 +++++--- .../membership_settings.json | 23 +++++++------------ 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index fc89396fa8..f058004ff9 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -99,11 +99,14 @@ class Membership(Document): if self.invoice and settings.send_invoice: attachments.append(frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format)) + email_template = frappe.get_doc("Email Template", settings.email_template) + context = { "doc": self, "member": member} + email_args = { "recipients": [email], - "message": settings.message, - "subject": _('Here is your invoice'), - "attachments": [frappe.attach_print("Sales Invoice", invoice.name, print_format=settings.inv_print_format)], + "message": frappe.render_template(email_template.get("response"), context), + "subject": frappe.render_template(email_template.get("subject"), context), + "attachments": attachments, "reference_doctype": self.doctype, "reference_name": self.name } diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 2452a763ce..5b6bab5b0a 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -19,8 +19,7 @@ "send_invoice", "membership_print_format", "inv_print_format", - "section_break_15", - "message" + "email_template" ], "fields": [ { @@ -96,7 +95,7 @@ "default": "0", "fieldname": "send_email", "fieldtype": "Check", - "label": "Send Acknowledge Email" + "label": "Send Membership Acknowledgement" }, { "depends_on": "eval: doc.send_invoice", @@ -111,26 +110,20 @@ "fieldname": "membership_print_format", "fieldtype": "Link", "label": "Membership Print Format", - "mandatory_depends_on": "eval:doc.send_email", "options": "Print Format" }, { "depends_on": "eval:doc.send_email", - "fieldname": "section_break_15", - "fieldtype": "Section Break", - "hide_border": 1 - }, - { - "depends_on": "eval:doc.send_email", - "fieldname": "message", - "fieldtype": "Text Editor", - "label": "Message" + "fieldname": "email_template", + "fieldtype": "Link", + "label": "Email Template", + "mandatory_depends_on": "eval:doc.send_email", + "options": "Email Template" } ], - "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-07-31 14:30:15.701767", + "modified": "2020-08-05 17:26:37.287395", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 3daa9de998e7acbbd440643258b131b863a5b363 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 6 Aug 2020 10:06:23 +0530 Subject: [PATCH 24/42] feat: add email option to field email_id --- erpnext/non_profit/doctype/member/member.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json index bb73a843ee..77cdb94b3d 100644 --- a/erpnext/non_profit/doctype/member/member.json +++ b/erpnext/non_profit/doctype/member/member.json @@ -133,7 +133,8 @@ { "fieldname": "email_id", "fieldtype": "Data", - "label": "Email Address" + "label": "Email Address", + "options": "Email" }, { "fieldname": "subscription_id", @@ -176,7 +177,7 @@ ], "image_field": "image", "links": [], - "modified": "2020-04-07 14:20:33.215700", + "modified": "2020-08-06 10:06:01.153564", "modified_by": "Administrator", "module": "Non Profit", "name": "Member", From aaeb3980bcdc0df493c52d20c4440b71f4ad4af2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 19 Aug 2020 15:13:30 +0530 Subject: [PATCH 25/42] feat: JSON download for HSN wise outward summary --- .../hsn_wise_summary_of_outward_supplies.js | 23 +++++ .../hsn_wise_summary_of_outward_supplies.py | 95 +++++++++++++++++-- 2 files changed, 108 insertions(+), 10 deletions(-) diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js index dfdf9dc095..b757d53aa2 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js @@ -46,5 +46,28 @@ frappe.query_reports["HSN-wise-summary of outward supplies"] = { ], onload: (report) => { fetch_gstins(report); + + report.page.add_inner_button(__("Download JSON"), function () { + var filters = report.get_values(); + + frappe.call({ + method: 'erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies.get_json', + args: { + data: report.data, + report_name: report.report_name, + filters: filters + }, + callback: function(r) { + if (r.message) { + const args = { + cmd: 'erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies.download_json_file', + data: r.message.data, + report_name: r.message.report_name + }; + open_url_post(frappe.request.url, args); + } + } + }); + }); } }; diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index a3ed4cebb1..6f3fff2932 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -4,11 +4,13 @@ from __future__ import unicode_literals import frappe, erpnext from frappe import _ -from frappe.utils import flt +from frappe.utils import flt, getdate, cstr from frappe.model.meta import get_field_precision from frappe.utils.xlsxutils import handle_html from six import iteritems import json +from erpnext.regional.india.utils import get_gst_accounts +from erpnext.regional.report.gstr_1.gstr_1 import get_company_gstin_number def execute(filters=None): return _execute(filters) @@ -141,7 +143,7 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic tax_details = frappe.db.sql(""" select - parent, description, item_wise_tax_detail, + parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount from `tab%s` where @@ -153,11 +155,11 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic """ % (tax_doctype, '%s', ', '.join(['%s']*len(invoice_item_row)), conditions), tuple([doctype] + list(invoice_item_row))) - for parent, description, item_wise_tax_detail, tax_amount in tax_details: - description = handle_html(description) - if description not in tax_columns and tax_amount: + for parent, account_head, item_wise_tax_detail, tax_amount in tax_details: + + if account_head not in tax_columns and tax_amount: # as description is text editor earlier and markup can break the column convention in reports - tax_columns.append(description) + tax_columns.append(account_head) if item_wise_tax_detail: try: @@ -175,17 +177,17 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic for d in item_row_map.get(parent, {}).get(item_code, []): item_tax_amount = tax_amount if item_tax_amount: - itemised_tax.setdefault((parent, item_code), {})[description] = frappe._dict({ + itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict({ "tax_amount": flt(item_tax_amount, tax_amount_precision) }) except ValueError: continue tax_columns.sort() - for desc in tax_columns: + for account_head in tax_columns: columns.append({ - "label": desc, - "fieldname": frappe.scrub(desc), + "label": account_head, + "fieldname": frappe.scrub(account_head), "fieldtype": "Float", "width": 110 }) @@ -212,3 +214,76 @@ def get_merged_data(columns, data): return result +@frappe.whitelist() +def get_json(filters, report_name, data): + filters = json.loads(filters) + report_data = json.loads(data) + gstin = filters.get('company_gstin') or get_company_gstin_number(filters["company"]) + + if not filters.get('from_date') or not filters.get('to_date'): + frappe.throw(_("Please enter From Date and To Date to generate JSON")) + + fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) + + gst_json = {"gstin": "", "version": "GST2.3.4", + "hash": "hash", "gstin": gstin, "fp": fp} + + gst_json["hsn"] = { + "data": get_hsn_wise_json_data(filters, report_data) + } + + return { + 'report_name': report_name, + 'data': gst_json + } + +@frappe.whitelist() +def download_json_file(): + ''' download json content in a file ''' + data = frappe._dict(frappe.local.form_dict) + frappe.response['filename'] = frappe.scrub("{0}".format(data['report_name'])) + '.json' + frappe.response['filecontent'] = data['data'] + frappe.response['content_type'] = 'application/json' + frappe.response['type'] = 'download' + +def get_hsn_wise_json_data(filters, report_data): + + filters = frappe._dict(filters) + gst_accounts = get_gst_accounts(filters.company) + data = [] + count = 1 + + for hsn in report_data: + row = { + "num": count, + "hsn_sc": hsn.get("gst_hsn_code"), + "desc": hsn.get("description"), + "uqc": hsn.get("stock_uom").upper(), + "qty": hsn.get("stock_qty"), + "val": flt(hsn.get("total_amount"), 2), + "txval": flt(hsn.get("taxable_amount", 2)), + "iamt": 0.0, + "camt": 0.0, + "samt": 0.0, + "csamt": 0.0 + + } + + for account in gst_accounts.get('igst_account'): + row['iamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('cgst_account'): + row['camt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('sgst_account'): + row['samt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('cess_account'): + row['csamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + data.append(row) + count +=1 + + return data + + From e292fd6c5877c346c74f3c7cde02e4b7677ca912 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 24 Aug 2020 00:45:13 +0530 Subject: [PATCH 26/42] feat: add integrations desk page --- .../erpnext_integrations.json | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json diff --git a/erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json new file mode 100644 index 0000000000..8dcc77d174 --- /dev/null +++ b/erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json @@ -0,0 +1,40 @@ +{ + "cards": [ + { + "hidden": 0, + "label": "Marketplace", + "links": "[\n {\n \"description\": \"Woocommerce marketplace settings\",\n \"label\": \"Woocommerce Settings\",\n \"name\": \"Woocommerce Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Amazon MWS settings\",\n \"label\": \"Amazon MWS Settings\",\n \"name\": \"Amazon MWS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Shopify settings\",\n \"label\": \"Shopify Settings\",\n \"name\": \"Shopify Settings\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Payments", + "links": "[\n {\n \"description\": \"GoCardless payment gateway settings\",\n \"label\": \"GoCardless Settings\",\n \"name\": \"GoCardless Settings\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Settings", + "links": "[\n {\n \"description\": \"Plaid settings\",\n \"label\": \"Plaid Settings\",\n \"name\": \"Plaid Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Exotel settings\",\n \"label\": \"Exotel Settings\",\n \"name\": \"Exotel Settings\",\n \"type\": \"doctype\"\n }\n]" + } + ], + "category": "Modules", + "charts": [], + "creation": "2020-08-20 19:30:48.138801", + "developer_mode_only": 0, + "disable_user_customization": 0, + "docstatus": 0, + "doctype": "Desk Page", + "extends": "Integrations", + "extends_another_page": 1, + "hide_custom": 1, + "idx": 0, + "is_standard": 1, + "label": "ERPNext Integrations", + "modified": "2020-08-23 16:30:51.494655", + "modified_by": "Administrator", + "module": "ERPNext Integrations", + "name": "ERPNext Integrations", + "owner": "Administrator", + "pin_to_bottom": 0, + "pin_to_top": 0, + "shortcuts": [] +} \ No newline at end of file From f49665077c59716db6a985639dfb4388a43db976 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 24 Aug 2020 18:26:48 +0530 Subject: [PATCH 27/42] feat: Quoted Item Comparison Report Enhancements v2 --- .../quoted_item_comparison.js | 58 +++++++++++-- .../quoted_item_comparison.py | 83 +++++++++++++------ 2 files changed, 109 insertions(+), 32 deletions(-) diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js index a76ffeec2e..8718e4e2ec 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js @@ -12,7 +12,22 @@ frappe.query_reports["Quoted Item Comparison"] = { "reqd": 1 }, { - reqd: 1, + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "width": "80", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "width": "80", + "reqd": 1, + "default": frappe.datetime.get_today() + }, + { default: "", options: "Item", label: __("Item"), @@ -45,13 +60,12 @@ frappe.query_reports["Quoted Item Comparison"] = { } }, { - fieldtype: "Link", + fieldtype: "MultiSelectList", label: __("Supplier Quotation"), - options: "Supplier Quotation", fieldname: "supplier_quotation", default: "", - get_query: () => { - return { filters: { "docstatus": ["<", 2] } } + get_data: function(txt) { + return frappe.db.get_link_options('Supplier Quotation', txt); } }, { @@ -63,9 +77,30 @@ frappe.query_reports["Quoted Item Comparison"] = { get_query: () => { return { filters: { "docstatus": ["<", 2] } } } + }, + { + fieldtype: "Check", + label: __("Include Expired"), + fieldname: "include_expired", + default: 0 } ], + formatter: (value, row, column, data, default_formatter) => { + value = default_formatter(value, row, column, data); + + if(column.fieldname === "valid_till" && data.valid_till){ + if(frappe.datetime.get_diff(data.valid_till, frappe.datetime.nowdate()) <= 1){ + value = `
${value}
`; + } + else if (frappe.datetime.get_diff(data.valid_till, frappe.datetime.nowdate()) <= 7){ + value = `
${value}
`; + } + } + + return value; + }, + onload: (report) => { // Create a button for setting the default supplier report.page.add_inner_button(__("Select Default Supplier"), () => { @@ -75,6 +110,19 @@ frappe.query_reports["Quoted Item Comparison"] = { reporter.make_default_supplier_dialog(report); }, 'Tools'); + const status_message = ` + + Valid till :    + + + Expires in a week + +      + + Expires today / Already Expired + ` + report.$status.html(status_message).show(); + }, make_default_supplier_dialog: (report) => { // Get the name of the item to change diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py index a33867a525..ffa138f1e9 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py @@ -16,44 +16,48 @@ def execute(filters=None): supplier_quotation_data = get_data(filters, conditions) columns = get_columns() - data, chart_data = prepare_data(supplier_quotation_data) + data, chart_data = prepare_data(supplier_quotation_data, filters) return columns, data, None, chart_data def get_conditions(filters): conditions = "" + if filters.get("item_code"): + conditions += " AND sqi.item_code = %(item_code)s" + if filters.get("supplier_quotation"): - conditions += " AND sqi.parent = %(supplier_quotation)s" + conditions += " AND sqi.parent in %(supplier_quotation)s" if filters.get("request_for_quotation"): conditions += " AND sqi.request_for_quotation = %(request_for_quotation)s" if filters.get("supplier"): conditions += " AND sq.supplier in %(supplier)s" + + if not filters.get("include_expired"): + conditions += " AND sq.status != 'Expired'" + return conditions def get_data(filters, conditions): - if not filters.get("item_code"): - return [] - supplier_quotation_data = frappe.db.sql("""SELECT - sqi.parent, sqi.qty, sqi.rate, sqi.uom, sqi.request_for_quotation, - sq.supplier + sqi.parent, sqi.item_code, sqi.qty, sqi.rate, sqi.uom, sqi.request_for_quotation, + sqi.lead_time_days, sq.supplier, sq.valid_till FROM `tabSupplier Quotation Item` sqi, `tabSupplier Quotation` sq WHERE - sqi.item_code = %(item_code)s - AND sqi.parent = sq.name + sqi.parent = sq.name AND sqi.docstatus < 2 AND sq.company = %(company)s - AND sq.status != 'Expired' - {0}""".format(conditions), filters, as_dict=1) + AND sq.transaction_date between %(from_date)s and %(to_date)s + {0} + order by sq.transaction_date, sqi.item_code""".format(conditions), filters, as_dict=1) return supplier_quotation_data -def prepare_data(supplier_quotation_data): - out, suppliers, qty_list = [], [], [] +def prepare_data(supplier_quotation_data, filters): + out, suppliers, qty_list, chart_data = [], [], [], [] supplier_wise_map = defaultdict(list) supplier_qty_price_map = {} @@ -70,20 +74,24 @@ def prepare_data(supplier_quotation_data): exchange_rate = 1 row = { + "item_code": data.get('item_code'), "quotation": data.get("parent"), "qty": data.get("qty"), "price": flt(data.get("rate") * exchange_rate, float_precision), "uom": data.get("uom"), "request_for_quotation": data.get("request_for_quotation"), + "valid_till": data.get('valid_till'), + "lead_time_days": data.get('lead_time_days') } # map for report view of form {'supplier1':[{},{},...]} supplier_wise_map[supplier].append(row) # map for chart preparation of the form {'supplier1': {'qty': 'price'}} - if not supplier in supplier_qty_price_map: - supplier_qty_price_map[supplier] = {} - supplier_qty_price_map[supplier][row["qty"]] = row["price"] + if filters.get("item_code"): + if not supplier in supplier_qty_price_map: + supplier_qty_price_map[supplier] = {} + supplier_qty_price_map[supplier][row["qty"]] = row["price"] suppliers.append(supplier) qty_list.append(data.get("qty")) @@ -97,7 +105,8 @@ def prepare_data(supplier_quotation_data): for entry in supplier_wise_map[supplier]: out.append(entry) - chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map) + if filters.get("item_code"): + chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map) return out, chart_data @@ -117,9 +126,10 @@ def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map): data_points_map[qty].append(None) dataset = [] + currency_symbol = frappe.db.get_value("Currency", frappe.db.get_default("currency"), "symbol") for qty in qty_list: datapoints = { - "name": _("Price for Qty ") + str(qty), + "name": currency_symbol + " (Qty " + str(qty) + " )", "values": data_points_map[qty] } dataset.append(datapoints) @@ -140,14 +150,21 @@ def get_columns(): "label": _("Supplier"), "fieldtype": "Link", "options": "Supplier", + "width": 150 + }, + { + "fieldname": "item_code", + "label": _("Item"), + "fieldtype": "Link", + "options": "Item", "width": 200 }, { - "fieldname": "quotation", - "label": _("Supplier Quotation"), + "fieldname": "uom", + "label": _("UOM"), "fieldtype": "Link", - "options": "Supplier Quotation", - "width": 200 + "options": "UOM", + "width": 90 }, { "fieldname": "qty", @@ -163,18 +180,30 @@ def get_columns(): "width": 110 }, { - "fieldname": "uom", - "label": _("UOM"), + "fieldname": "quotation", + "label": _("Supplier Quotation"), "fieldtype": "Link", - "options": "UOM", - "width": 90 + "options": "Supplier Quotation", + "width": 200 + }, + { + "fieldname": "valid_till", + "label": _("Valid Till"), + "fieldtype": "Date", + "width": 100 + }, + { + "fieldname": "lead_time_days", + "label": _("Lead Time (Days)"), + "fieldtype": "Int", + "width": 100 }, { "fieldname": "request_for_quotation", "label": _("Request for Quotation"), "fieldtype": "Link", "options": "Request for Quotation", - "width": 200 + "width": 150 } ] From 8f452b86630b306dd47c6360d074a9d1ec2ad040 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 24 Aug 2020 18:57:07 +0530 Subject: [PATCH 28/42] fix: Codacy and indicator message --- .../report/quoted_item_comparison/quoted_item_comparison.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js index 8718e4e2ec..ad390c446b 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js @@ -115,12 +115,12 @@ frappe.query_reports["Quoted Item Comparison"] = { Valid till :    - Expires in a week + Expires in a week or less      Expires today / Already Expired - ` + `; report.$status.html(status_message).show(); }, From 2030b66fc6ca831c21e6a8d0a1dab82e0601f2b3 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 25 Aug 2020 17:09:53 +0530 Subject: [PATCH 29/42] feat: add customer link to call log --- .../communication/doctype/call_log/call_log.json | 16 ++++++++++++++-- .../communication/doctype/call_log/call_log.py | 3 +++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/erpnext/communication/doctype/call_log/call_log.json b/erpnext/communication/doctype/call_log/call_log.json index cfc08eb084..31e79f17cd 100644 --- a/erpnext/communication/doctype/call_log/call_log.json +++ b/erpnext/communication/doctype/call_log/call_log.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "field:id", "creation": "2019-06-05 12:07:02.634534", "doctype": "DocType", @@ -14,6 +15,7 @@ "contact", "contact_name", "column_break_10", + "customer", "lead", "lead_name", "section_break_5", @@ -28,7 +30,8 @@ }, { "fieldname": "section_break_5", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Call Details" }, { "fieldname": "id", @@ -125,10 +128,19 @@ "in_list_view": 1, "label": "Lead Name", "read_only": 1 + }, + { + "fieldname": "customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer", + "read_only": 1 } ], "in_create": 1, - "modified": "2019-08-06 05:46:53.144683", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-08-25 17:08:34.085731", "modified_by": "Administrator", "module": "Communication", "name": "Call Log", diff --git a/erpnext/communication/doctype/call_log/call_log.py b/erpnext/communication/doctype/call_log/call_log.py index 5fe3c4edbb..b31b757a37 100644 --- a/erpnext/communication/doctype/call_log/call_log.py +++ b/erpnext/communication/doctype/call_log/call_log.py @@ -16,6 +16,9 @@ class CallLog(Document): self.contact = get_contact_with_phone_number(number) self.lead = get_lead_with_phone_number(number) + contact = frappe.get_doc("Contact", self.contact) + self.customer = contact.get_link_for("Customer") + def after_insert(self): self.trigger_call_popup() From de4ac0c9052aede26eee0208d3b66013a7fd908e Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 25 Aug 2020 17:10:05 +0530 Subject: [PATCH 30/42] chore: remove stray tabs --- erpnext/selling/doctype/customer/customer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 93d4832173..911fe511ae 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -184,10 +184,10 @@ class Customer(TransactionBase): def validate_credit_limit_on_change(self): if self.get("__islocal") or not self.credit_limits: return - + past_credit_limits = [d.credit_limit for d in frappe.db.get_all("Customer Credit Limit", filters={'parent': self.name}, fields=["credit_limit"], order_by="company")] - + current_credit_limits = [d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company)] if past_credit_limits == current_credit_limits: From f4b939754c84111e9417734ea248e1c85a16ce1d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 26 Aug 2020 12:10:12 +0530 Subject: [PATCH 31/42] fix: Codacy fixes --- .../hsn_wise_summary_of_outward_supplies.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index 6f3fff2932..59389ce326 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -225,7 +225,7 @@ def get_json(filters, report_name, data): fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) - gst_json = {"gstin": "", "version": "GST2.3.4", + gst_json = {"version": "GST2.3.4", "hash": "hash", "gstin": gstin, "fp": fp} gst_json["hsn"] = { @@ -239,7 +239,7 @@ def get_json(filters, report_name, data): @frappe.whitelist() def download_json_file(): - ''' download json content in a file ''' + '''download json content in a file''' data = frappe._dict(frappe.local.form_dict) frappe.response['filename'] = frappe.scrub("{0}".format(data['report_name'])) + '.json' frappe.response['filecontent'] = data['data'] From c71e37c988c79ed126d38ffe59c3b1b74f7718a0 Mon Sep 17 00:00:00 2001 From: michellealva Date: Sun, 30 Aug 2020 19:42:24 +0530 Subject: [PATCH 32/42] feat: Allow Rename for Tax Category --- .../doctype/tax_category/tax_category.json | 90 +++---------------- 1 file changed, 11 insertions(+), 79 deletions(-) diff --git a/erpnext/accounts/doctype/tax_category/tax_category.json b/erpnext/accounts/doctype/tax_category/tax_category.json index 1e3ae455b3..6f682a0466 100644 --- a/erpnext/accounts/doctype/tax_category/tax_category.json +++ b/erpnext/accounts/doctype/tax_category/tax_category.json @@ -1,134 +1,66 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "actions": [], + "allow_rename": 1, "autoname": "field:title", - "beta": 0, "creation": "2018-11-22 23:38:39.668804", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "title" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "title", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Title", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, "unique": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2020-01-15 17:14:28.951793", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-08-30 19:41:25.783852", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Category", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Accounts Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Accounts User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 0 + "share": 1 } ], "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} + "track_changes": 1 +} \ No newline at end of file From a4259208e7482df727d8efc1beace9eb4906467c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 30 Aug 2020 23:09:23 +0530 Subject: [PATCH 33/42] feat: Utility function to get possible loan disbursal amount --- .../loan_management/doctype/loan/test_loan.py | 51 ++++++++++++++++++ .../loan_application/loan_application.js | 10 ++-- .../loan_disbursement/loan_disbursement.py | 53 +++++++++++-------- .../loan_interest_accrual.py | 14 +++-- .../doctype/loan_repayment/loan_repayment.py | 7 ++- .../loan_security_shortfall.py | 12 +++-- 6 files changed, 111 insertions(+), 36 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 23815d5982..b75f7bdd75 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -17,6 +17,7 @@ from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loa 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 from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge +from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount class TestLoan(unittest.TestCase): def setUp(self): @@ -323,6 +324,56 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan.status, 'Closed') self.assertEquals(sum(pledged_qty.values()), 0) + def test_disbursal_check_with_shortfall(self): + pledges = [{ + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, + 'Stock Loan', pledges, "Repay Over Number of Periods", 12) + + create_pledge(loan_application) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan.submit() + + #Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge + make_loan_disbursement_entry(loan.name, 700000) + + frappe.db.sql("""UPDATE `tabLoan Security Price` SET loan_security_price = 100 + where loan_security='Test Security 2'""") + + create_process_loan_security_shortfall() + loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name}) + self.assertTrue(loan_security_shortfall) + + self.assertEqual(get_disbursal_amount(loan.name), 0) + + frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250 + where loan_security='Test Security 2'""") + + def test_disbursal_check_without_shortfall(self): + pledges = [{ + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, + 'Stock Loan', pledges, "Repay Over Number of Periods", 12) + + create_pledge(loan_application) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan.submit() + + #Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge + make_loan_disbursement_entry(loan.name, 700000) + + self.assertEqual(get_disbursal_amount(loan.name), 300000) + def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.js b/erpnext/loan_management/doctype/loan_application/loan_application.js index b56fce1d7c..1365274971 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.js +++ b/erpnext/loan_management/doctype/loan_application/loan_application.js @@ -33,18 +33,18 @@ frappe.ui.form.on('Loan Application', { if (frm.doc.is_secured_loan) { frappe.db.get_value("Loan Security Pledge", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { - if (!r) { + if (Object.keys(r).length === 0) { frm.add_custom_button(__('Loan Security Pledge'), function() { - frm.trigger('create_loan_security_pledge') + frm.trigger('create_loan_security_pledge'); },__('Create')) } }); } frappe.db.get_value("Loan", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { - if (!r) { + if (Object.keys(r).length === 0) { frm.add_custom_button(__('Loan'), function() { - frm.trigger('create_loan') + frm.trigger('create_loan'); },__('Create')) } else { frm.set_df_property('status', 'read_only', 1); @@ -54,7 +54,7 @@ frappe.ui.form.on('Loan Application', { }, create_loan: function(frm) { if (frm.doc.status != "Approved") { - frappe.throw(__("Cannot create loan until application is approved")) + frappe.throw(__("Cannot create loan until application is approved")); } frappe.model.open_mapped_doc({ diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index 6c27e12134..260fada893 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -67,28 +67,10 @@ class LoanDisbursement(AccountsController): disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount total_payment = loan_details.total_payment - if disbursed_amount > loan_details.loan_amount and loan_details.is_term_loan: - frappe.throw(_("Disbursed Amount cannot be greater than loan amount")) + possible_disbursal_amount = get_disbursal_amount(self.against_loan) - if loan_details.status == 'Disbursed': - pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ - - flt(loan_details.total_principal_paid) - else: - pending_principal_amount = loan_details.disbursed_amount - - security_value = 0.0 - if loan_details.is_secured_loan: - security_value = get_total_pledged_security_value(self.against_loan) - - if not security_value: - security_value = loan_details.loan_amount - - if pending_principal_amount + self.disbursed_amount > flt(security_value): - allowed_amount = security_value - pending_principal_amount - if allowed_amount < 0: - allowed_amount = 0 - - frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(allowed_amount)) + if self.disbursed_amount > possible_disbursal_amount: + frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount)) if loan_details.status == "Disbursed" and not loan_details.is_term_loan: process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1), @@ -176,3 +158,32 @@ def get_total_pledged_security_value(loan): security_value += (loan_security_price_map.get(security) * qty * hair_cut_map.get(security))/100 return security_value + +@frappe.whitelist() +def get_disbursal_amount(loan): + loan_details = frappe.get_all("Loan", fields = ["loan_amount", "disbursed_amount", "total_payment", + "total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan"], + filters= { "name": loan })[0] + + if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan, + 'status': 'Pending'}): + return 0 + + if loan_details.status == 'Disbursed': + pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ + - flt(loan_details.total_principal_paid) + else: + pending_principal_amount = flt(loan_details.disbursed_amount) + + security_value = 0.0 + if loan_details.is_secured_loan: + security_value = get_total_pledged_security_value(loan) + + if not security_value and not loan_details.is_secured_loan: + security_value = flt(loan_details.loan_amount) + + disbursal_amount = flt(security_value) - flt(pending_principal_amount) + + return disbursal_amount + + diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index c5111fdc93..1d3fa71068 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -85,8 +85,11 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i if no_of_days <= 0: return - pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ - - flt(loan.total_principal_paid) + if loan.status == 'Disbursed': + pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) + else: + pending_principal_amount = loan.disbursed_amount interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100) payable_interest = interest_per_day * no_of_days @@ -107,7 +110,7 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None): query_filters = { - "status": "Disbursed", + "status": ('in', ['Disbursed', 'Partially Disbursed']), "docstatus": 1 } @@ -118,8 +121,9 @@ def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_inte if not open_loans: open_loans = frappe.get_all("Loan", - fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", "is_term_loan", - "disbursement_date", "applicant_type", "applicant", "rate_of_interest", "total_interest_payable", "repayment_start_date"], + fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", + "is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant", + "rate_of_interest", "total_interest_payable", "total_principal_paid", "repayment_start_date"], filters=query_filters) for loan in open_loans: diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 9605045777..451ae85afb 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -281,7 +281,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): 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)) + 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 @@ -297,7 +297,10 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): if not final_due_date: final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) - pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable + if against_loan_doc.status == 'Disbursed': + pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable + else: + pending_principal_amount = against_loan_doc.disbursed_amount if payment_type == "Loan Closure": if due_date: 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 02efe240bd..c3ea882809 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 @@ -51,13 +51,19 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): "valid_upto": (">=", update_time) }, as_list=1)) - loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid'], - filters={'status': 'Disbursed', 'is_secured_loan': 1}) + loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid', 'total_payment', + 'total_interest_payable', 'disbursed_amount'], + filters={'status': ('in',['Disbursed','Partially Disbursed']), 'is_secured_loan': 1}) loan_security_map = {} for loan in loans: - outstanding_amount = loan.loan_amount - loan.total_principal_paid + if loan.status == 'Disbursed': + outstanding_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) + else: + outstanding_amount = loan.disbursed_amount + pledged_securities = get_pledged_security_qty(loan.name) ltv_ratio = '' security_value = 0.0 From ce29757bff235030d49bc1caf3d2a17d4fd2b941 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 31 Aug 2020 11:26:51 +0530 Subject: [PATCH 34/42] fix: Import flt --- .../doctype/loan_security_shortfall/loan_security_shortfall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c3ea882809..71e741ccf0 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 @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import get_datetime +from frappe.utils import get_datetime, flt from frappe.model.document import Document from six import iteritems from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty From e8b121c2c2dc4fd00f78f32c8d7a26089c9ecddb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 31 Aug 2020 11:31:48 +0530 Subject: [PATCH 35/42] fix: Add status in field list --- .../doctype/loan_security_shortfall/loan_security_shortfall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 71e741ccf0..0f42bde3c4 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 @@ -52,7 +52,7 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): }, as_list=1)) loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid', 'total_payment', - 'total_interest_payable', 'disbursed_amount'], + 'total_interest_payable', 'disbursed_amount', 'status'], filters={'status': ('in',['Disbursed','Partially Disbursed']), 'is_secured_loan': 1}) loan_security_map = {} From d70e711aea9221b4a0d42f56081eb28d6b059b2e Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 31 Aug 2020 13:14:32 +0530 Subject: [PATCH 36/42] fix: events not deleted on cancelling maintenance schedule (#22954) * feat: add participant to event_participant child table * feat: add tests * chore: update function name Co-authored-by: Marica Co-authored-by: Marica --- .../maintenance_schedule.py | 8 ++-- .../test_maintenance_schedule.py | 38 ++++++++++++++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index add7bbfa57..cba6a2d014 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -67,16 +67,16 @@ class MaintenanceSchedule(TransactionBase): for key in scheduled_date: description =frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(self.name, d.item_code, self.customer) - frappe.get_doc({ + event = frappe.get_doc({ "doctype": "Event", "owner": email_map.get(d.sales_person, self.owner), "subject": description, "description": description, "starts_on": cstr(key["scheduled_date"]) + " 10:00:00", "event_type": "Private", - "ref_type": self.doctype, - "ref_name": self.name - }).insert(ignore_permissions=1) + }) + event.add_participant(self.doctype, self.name) + event.insert(ignore_permissions=1) frappe.db.set(self, 'status', 'Submitted') diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py index d8ae17b4c7..3c307e920f 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py @@ -2,6 +2,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals +from frappe.utils.data import get_datetime, add_days import frappe import unittest @@ -9,4 +10,39 @@ import unittest # test_records = frappe.get_test_records('Maintenance Schedule') class TestMaintenanceSchedule(unittest.TestCase): - pass + def test_events_should_be_created_and_deleted(self): + ms = make_maintenance_schedule() + ms.generate_schedule() + ms.submit() + + all_events = get_events(ms) + self.assertTrue(len(all_events) > 0) + + ms.cancel() + events_after_cancel = get_events(ms) + self.assertTrue(len(events_after_cancel) == 0) + +def get_events(ms): + return frappe.get_all("Event Participants", filters={ + "reference_doctype": ms.doctype, + "reference_docname": ms.name, + "parenttype": "Event" + }) + +def make_maintenance_schedule(): + ms = frappe.new_doc("Maintenance Schedule") + ms.company = "_Test Company" + ms.customer = "_Test Customer" + ms.transaction_date = get_datetime() + + ms.append("items", { + "item_code": "_Test Item", + "start_date": get_datetime(), + "end_date": add_days(get_datetime(), 32), + "periodicity": "Weekly", + "no_of_visits": 4, + "sales_person": "Sales Team", + }) + ms.insert(ignore_permissions=True) + + return ms From f9f26d2fa419aaec5c27b6c51086c979bb78907a Mon Sep 17 00:00:00 2001 From: Anupam K Date: Mon, 31 Aug 2020 14:00:27 +0530 Subject: [PATCH 37/42] fix: contact us button issue --- .../generators/item/item_add_to_cart.html | 31 ++++++++++--------- .../generators/item/item_configure.html | 7 ++--- .../generators/item/item_inquiry.html | 11 +++++++ 3 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 erpnext/templates/generators/item/item_inquiry.html diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html index 2a70d8dbe9..40bc0c749b 100644 --- a/erpnext/templates/generators/item/item_add_to_cart.html +++ b/erpnext/templates/generators/item/item_add_to_cart.html @@ -27,22 +27,25 @@ {% endif %} {% endif %} - {% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %}
- - {{ _("View in Cart") }} - - + {% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %} + + {{ _("View in Cart") }} + + + {% endif %} + {% if cart_settings.show_contact_us_button %} + {% include "templates/generators/item/item_inquiry.html" %} + {% endif %}
- {% endif %} diff --git a/erpnext/templates/generators/item/item_configure.html b/erpnext/templates/generators/item/item_configure.html index b8b0d98bdc..73f9ec99b3 100644 --- a/erpnext/templates/generators/item/item_configure.html +++ b/erpnext/templates/generators/item/item_configure.html @@ -10,14 +10,11 @@ {{ _('Configure') }} {% endif %} - {% if cart_settings.show_contact_us_button | int %} - + {% if cart_settings.show_contact_us_button %} + {% include "templates/generators/item/item_inquiry.html" %} {% endif %} {% endif %} diff --git a/erpnext/templates/generators/item/item_inquiry.html b/erpnext/templates/generators/item/item_inquiry.html new file mode 100644 index 0000000000..83653b6821 --- /dev/null +++ b/erpnext/templates/generators/item/item_inquiry.html @@ -0,0 +1,11 @@ +{% if shopping_cart and shopping_cart.cart_settings.enabled %} +{% set cart_settings = shopping_cart.cart_settings %} + {% if cart_settings.show_contact_us_button | int %} + + {% endif %} + +{% endif %} From 25042a22eb407486bb80099b8e8b416aec022f50 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 31 Aug 2020 19:17:29 +0530 Subject: [PATCH 38/42] fix: Pending amount after loan closure request --- .../loan_management/doctype/loan_repayment/loan_repayment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 451ae85afb..d1bb6ccedf 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -297,7 +297,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): if not final_due_date: final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) - if against_loan_doc.status == 'Disbursed': + if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested'): pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable else: pending_principal_amount = against_loan_doc.disbursed_amount From bc6c4e864dfe36835bffc7e0af03a27b32d0eb38 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 31 Aug 2020 20:32:17 +0530 Subject: [PATCH 39/42] fix: Status in Report and filter query --- .../quoted_item_comparison.js | 15 +-------------- .../quoted_item_comparison.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js index ad390c446b..518d665e7e 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js @@ -65,7 +65,7 @@ frappe.query_reports["Quoted Item Comparison"] = { fieldname: "supplier_quotation", default: "", get_data: function(txt) { - return frappe.db.get_link_options('Supplier Quotation', txt); + return frappe.db.get_link_options('Supplier Quotation', txt, {'docstatus': ["<", 2]}); } }, { @@ -110,19 +110,6 @@ frappe.query_reports["Quoted Item Comparison"] = { reporter.make_default_supplier_dialog(report); }, 'Tools'); - const status_message = ` - - Valid till :    - - - Expires in a week or less - -      - - Expires today / Already Expired - `; - report.$status.html(status_message).show(); - }, make_default_supplier_dialog: (report) => { // Get the name of the item to change diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py index ffa138f1e9..4426560c16 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py @@ -17,8 +17,9 @@ def execute(filters=None): columns = get_columns() data, chart_data = prepare_data(supplier_quotation_data, filters) + message = get_message() - return columns, data, None, chart_data + return columns, data, message, chart_data def get_conditions(filters): conditions = "" @@ -207,4 +208,16 @@ def get_columns(): } ] - return columns \ No newline at end of file + return columns + +def get_message(): + return """ + Valid till :    + + + Expires in a week or less + +    + + Expires today / Already Expired + """ \ No newline at end of file From 1dc9a9b669cb502e905318952bda9d8aa2234b22 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Mon, 31 Aug 2020 20:35:05 +0530 Subject: [PATCH 40/42] fix: reverse journal entry for multi-currency (#23165) * fix: reverse journal entry for multi-currency * fix: test case for reverse journal entry --- .../doctype/journal_entry/journal_entry.js | 20 +++------ .../doctype/journal_entry/journal_entry.py | 31 +++++++++++++ .../journal_entry/test_journal_entry.py | 43 +++++++++++++++++++ 3 files changed, 80 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index a09face791..409c15f75c 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -638,20 +638,12 @@ $.extend(erpnext.journal_entry, { return { filters: filters }; }, - reverse_journal_entry: function(frm) { - var me = frm.doc; - for(var i=0; i Date: Mon, 31 Aug 2020 21:21:05 +0530 Subject: [PATCH 41/42] fix: Add unit test for pending principal amount --- .../loan_management/doctype/loan/test_loan.py | 53 +++++++++++++++---- .../doctype/loan_repayment/loan_repayment.py | 8 +++ .../loan_security_unpledge.py | 8 +-- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index b75f7bdd75..2f6cd25a36 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -18,6 +18,7 @@ from erpnext.loan_management.doctype.loan.loan import create_loan_security_unple from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount +from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts class TestLoan(unittest.TestCase): def setUp(self): @@ -194,18 +195,14 @@ class TestLoan(unittest.TestCase): 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() amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', 'paid_principal_amount']) - unaccrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 6) \ - / (days_in_year(get_datetime(first_date).year) * 100) - - self.assertEquals(flt(amounts[0] + unaccrued_interest_amount, 3), - flt(accrued_interest_amount, 3)) + self.assertEquals(flt(amounts[0], 2),flt(accrued_interest_amount, 2)) self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) loan.load_from_db() @@ -307,9 +304,6 @@ class TestLoan(unittest.TestCase): "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() - amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', - 'paid_principal_amount']) - loan.load_from_db() self.assertEquals(loan.status, "Loan Closure Requested") @@ -374,6 +368,47 @@ class TestLoan(unittest.TestCase): self.assertEqual(get_disbursal_amount(loan.name), 300000) + def test_pending_loan_amount_after_closure_request(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + + no_of_days += 6 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + + amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") + + 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() + + amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', + 'paid_principal_amount']) + + loan.load_from_db() + self.assertEquals(loan.status, "Loan Closure Requested") + + amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") + self.assertEquals(amounts['pending_principal_amount'], 0.0) def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index d1bb6ccedf..7d83e32213 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -116,6 +116,7 @@ class LoanRepayment(AccountsController): def allocate_amounts(self, paid_entries): self.set('repayment_details', []) self.principal_amount_paid = 0 + total_interest_paid = 0 interest_paid = self.amount_paid - self.penalty_amount if self.amount_paid - self.penalty_amount > 0 and paid_entries: @@ -137,12 +138,19 @@ class LoanRepayment(AccountsController): interest_paid = 0 paid_principal=0 + total_interest_paid += interest_amount self.append('repayment_details', { 'loan_interest_accrual': lia, 'paid_interest_amount': interest_amount, 'paid_principal_amount': paid_principal }) + if self.payment_type == 'Loan Closure' and total_interest_paid < self.interest_payable: + unaccrued_interest = self.interest_payable - total_interest_paid + interest_paid -= unaccrued_interest + if self.repayment_details: + self.repayment_details[-1].paid_interest_amount += unaccrued_interest + if interest_paid: self.principal_amount_paid += interest_paid 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 5e9d82aa91..f6b28dae75 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 @@ -43,8 +43,10 @@ class LoanSecurityUnpledge(Document): "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 + total_payment, principal_paid, interest_payable = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', + 'total_interest_payable']) + + pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) security_value = 0 for security in self.securities: @@ -60,7 +62,7 @@ class LoanSecurityUnpledge(Document): security_value += qty_after_unpledge * loan_security_price_map.get(security.loan_security) - if not security_value and pending_principal_amount > 0: + if not security_value and flt(pending_principal_amount, 2) > 0: frappe.throw("Cannot Unpledge, loan to value ratio is breaching") if security_value and (pending_principal_amount/security_value) * 100 > ltv_ratio: From eb4b2aa14252161433bb3f3f5e2be45476f0af8b Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 1 Sep 2020 15:01:32 +0530 Subject: [PATCH 42/42] fix: get_items from product bundle for purchase order (#22821) * fix: get_items from product bundle for purchase order * fix: Don't overwrite doctype while setting attributes in child row Co-authored-by: Marica --- erpnext/public/js/controllers/buying.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index a4cc68b3e2..cb76c87b62 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -503,11 +503,11 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { if(!r.exc && r.message) { remove_empty_first_row(frm); - for ( var i=0; i< r.message.length; i++ ) { + for (var i=0; i< r.message.length; i++) { var d = frm.add_child("items"); var item = r.message[i]; - for ( var key in item) { - if ( !is_null(item[key]) ) { + for (var key in item) { + if (!is_null(item[key]) && key !== "doctype") { d[key] = item[key]; } }