diff --git a/lending/hooks.py b/lending/hooks.py index f88e62403..e60cb1c96 100644 --- a/lending/hooks.py +++ b/lending/hooks.py @@ -194,7 +194,12 @@ ], "hourly_long": [ "lending.loan_management.doctype.loan_repayment.loan_repayment.process_pending_credit_notes", - ] + ], + "cron": { + "*/30 * * * *": [ + "lending.loan_management.utils.process_cancelled_gl_entries", + ] + }, } bank_reconciliation_doctypes = [ diff --git a/lending/install.py b/lending/install.py index 0de3f748c..87b00282d 100644 --- a/lending/install.py +++ b/lending/install.py @@ -95,12 +95,26 @@ "fieldtype": "Check", "insert_after": "loan_column_break", }, + { + "fieldname": "enable_demand_cancel_gl_queue", + "label": "Enable Demand Cancel GL Queue", + "fieldtype": "Check", + "default": "1", + "insert_after": "enable_loan_accounting", + }, + { + "fieldname": "enable_interest_cancel_gl_queue", + "label": "Enable Interest Cancel GL Queue", + "fieldtype": "Check", + "default": "1", + "insert_after": "enable_demand_cancel_gl_queue", + }, { "fieldname": "collection_offset_logic_based_on", "label": "Collection Offset Logic Based On", "fieldtype": "Select", "options": "NPA Flag\nDays Past Due", - "insert_after": "enable_loan_accounting", + "insert_after": "enable_interest_cancel_gl_queue", }, { "fieldname": "days_past_due_threshold", diff --git a/lending/loan_management/doctype/loan_demand/loan_demand.json b/lending/loan_management/doctype/loan_demand/loan_demand.json index ab77c455c..893a5270c 100644 --- a/lending/loan_management/doctype/loan_demand/loan_demand.json +++ b/lending/loan_management/doctype/loan_demand/loan_demand.json @@ -36,6 +36,7 @@ "accounting_dimensions_section", "cost_center", "is_imported", + "cancel_gl_pending", "column_break_hmrs", "amended_from", "process_loan_demand", @@ -294,6 +295,14 @@ "label": "Is Imported", "read_only": 1 }, + { + "default": "0", + "fieldname": "cancel_gl_pending", + "fieldtype": "Check", + "hidden": 1, + "label": "Cancel GL Pending", + "read_only": 1 + }, { "default": "0", "fieldname": "is_partial_pre_paid_interest", @@ -305,7 +314,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-02-16 15:13:09.765918", + "modified": "2026-05-25 10:35:43.539786", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Demand", diff --git a/lending/loan_management/doctype/loan_demand/loan_demand.py b/lending/loan_management/doctype/loan_demand/loan_demand.py index eedc64515..098ed2922 100644 --- a/lending/loan_management/doctype/loan_demand/loan_demand.py +++ b/lending/loan_management/doctype/loan_demand/loan_demand.py @@ -22,6 +22,7 @@ class LoanDemand(LoanController): amended_from: DF.Link | None applicant: DF.DynamicLink | None applicant_type: DF.Link | None + cancel_gl_pending: DF.Check company: DF.Link | None cost_center: DF.Link | None demand_amount: DF.Currency @@ -101,10 +102,18 @@ def update_repayment_schedule(self, cancel=0): def on_cancel(self): self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"] - self.make_gl_entries(cancel=1) + + if self.queue_cancel_gl() and not frappe.flags.in_test: + self.db_set("cancel_gl_pending", 1) + else: + self.make_gl_entries(cancel=1) + self.update_repayment_schedule(cancel=1) self.make_credit_note() + def queue_cancel_gl(self): + return frappe.db.get_value("Company", self.company, "enable_demand_cancel_gl_queue") + def make_credit_note(self): if not self.demand_type == "Charges": return diff --git a/lending/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/lending/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json index fe3043405..ccaed36c5 100644 --- a/lending/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json +++ b/lending/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json @@ -41,6 +41,7 @@ "process_loan_interest_accrual", "amended_from", "is_imported", + "cancel_gl_pending", "column_break_svgc", "normal_interest_journal_entry", "additional_interest_suspense_entry" @@ -291,13 +292,21 @@ "fieldtype": "Check", "label": "Is Imported", "read_only": 1 + }, + { + "default": "0", + "fieldname": "cancel_gl_pending", + "fieldtype": "Check", + "hidden": 1, + "label": "Cancel GL Pending", + "read_only": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-01-28 15:20:16.650685", + "modified": "2026-05-25 10:36:03.518964", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Interest Accrual", diff --git a/lending/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/lending/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index ec4511fa5..d9521c24b 100644 --- a/lending/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/lending/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -43,6 +43,7 @@ class LoanInterestAccrual(LoanController): applicant: DF.DynamicLink | None applicant_type: DF.Literal["Employee", "Member", "Customer"] base_amount: DF.Currency + cancel_gl_pending: DF.Check company: DF.Link | None cost_center: DF.Link | None interest_amount: DF.Currency @@ -145,7 +146,12 @@ def on_submit(self): self.db_set("additional_interest_suspense_entry", additional_interest_jv) def on_cancel(self): - self.make_gl_entries(cancel=1) + self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"] + + if self.queue_cancel_gl() and not frappe.flags.in_test: + self.db_set("cancel_gl_pending", 1) + else: + self.make_gl_entries(cancel=1) if self.normal_interest_journal_entry and loan_accounting_enabled(self.company): doc = frappe.get_doc("Journal Entry", self.normal_interest_journal_entry) @@ -157,7 +163,8 @@ def on_cancel(self): doc.flags.ignore_links = True doc.cancel() - self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"] + def queue_cancel_gl(self): + return frappe.db.get_value("Company", self.company, "enable_interest_cancel_gl_queue") def make_gl_entries(self, cancel=0, adv_adj=0): if not loan_accounting_enabled(self.company): diff --git a/lending/loan_management/utils.py b/lending/loan_management/utils.py index 2699c29fe..6072b5f64 100644 --- a/lending/loan_management/utils.py +++ b/lending/loan_management/utils.py @@ -367,3 +367,44 @@ def create_charge_master(charge_type): "is_stock_item": 0, } ).insert() + + +def process_cancelled_gl_entries(): + process_cancelled_documents( + doctype="Loan Demand", + title="Loan Demand Cancel GL Queue Error", + ) + + process_cancelled_documents( + doctype="Loan Interest Accrual", + title="Loan Interest Accrual Cancel GL Queue Error", + ) + + +def process_cancelled_documents(doctype, title): + doc = frappe.qb.DocType(doctype) + rows = ( + frappe.qb.from_(doc) + .select(doc.name) + .where((doc.docstatus == 2) & (doc.cancel_gl_pending == 1)) + .run(as_dict=True) + ) + + for row in rows: + try: + docname = row.name + document = frappe.get_doc(doctype, docname, for_update=True) + if not document.cancel_gl_pending: + frappe.db.rollback() + continue + document.make_gl_entries(cancel=1) + document.db_set("cancel_gl_pending", 0) + frappe.db.commit() + except Exception: + frappe.db.rollback() + frappe.log_error( + title=title, + message=frappe.get_traceback(), + reference_doctype=doctype, + reference_name=row.name, + ) diff --git a/lending/patches.txt b/lending/patches.txt index 115456d2d..4de316418 100644 --- a/lending/patches.txt +++ b/lending/patches.txt @@ -48,3 +48,4 @@ lending.patches.v1_0.update_value_date_in_loan_refund lending.patches.v1_0.update_value_date_in_pending_doctypes lending.patches.v16_0.add_enable_loan_accounting_field lending.patches.v16_0.update_is_invoice_generated_field +lending.patches.v16_0.add_cancel_gl_queue_settings_in_company diff --git a/lending/patches/v16_0/add_cancel_gl_queue_settings_in_company.py b/lending/patches/v16_0/add_cancel_gl_queue_settings_in_company.py new file mode 100644 index 000000000..6e359c29c --- /dev/null +++ b/lending/patches/v16_0/add_cancel_gl_queue_settings_in_company.py @@ -0,0 +1,57 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + + +def execute(): + company_fields = [] + created_demand_queue_field = False + created_interest_queue_field = False + + if not frappe.db.exists( + "Custom Field", {"dt": "Company", "fieldname": "enable_demand_cancel_gl_queue"} + ): + company_fields.append( + { + "fieldname": "enable_demand_cancel_gl_queue", + "label": "Enable Demand Cancel GL Queue", + "fieldtype": "Check", + "default": "1", + "insert_after": "enable_loan_accounting", + } + ) + created_demand_queue_field = True + + if not frappe.db.exists( + "Custom Field", {"dt": "Company", "fieldname": "enable_interest_cancel_gl_queue"} + ): + company_fields.append( + { + "fieldname": "enable_interest_cancel_gl_queue", + "label": "Enable Interest Cancel GL Queue", + "fieldtype": "Check", + "default": "1", + "insert_after": "enable_demand_cancel_gl_queue", + } + ) + created_interest_queue_field = True + + if company_fields: + create_custom_fields({"Company": company_fields}, update=True) + + if created_demand_queue_field: + frappe.db.sql( + """ + update `tabCompany` + set enable_demand_cancel_gl_queue = 1 + where ifnull(enable_demand_cancel_gl_queue, 0) = 0 + """ + ) + + if created_interest_queue_field: + frappe.db.sql( + """ + update `tabCompany` + set enable_interest_cancel_gl_queue = 1 + where ifnull(enable_interest_cancel_gl_queue, 0) = 0 + """ + )