Merge pull request #40372 from ruthra-kumar/toggle_for_standalone_cr_dr_notes
refactor: checkbox to toggle standalone Credit/Debit note behaviour
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index d12a43c..22f2d13 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -22,6 +22,7 @@
"is_paid",
"is_return",
"return_against",
+ "update_outstanding_for_self",
"update_billed_amount_in_purchase_order",
"update_billed_amount_in_purchase_receipt",
"apply_tds",
@@ -1623,13 +1624,21 @@
"fieldtype": "Link",
"label": "Supplier Group",
"options": "Supplier Group"
+ },
+ {
+ "default": "1",
+ "depends_on": "eval: doc.is_return && doc.return_against",
+ "description": "Debit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
+ "fieldname": "update_outstanding_for_self",
+ "fieldtype": "Check",
+ "label": "Update Outstanding for Self"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2024-02-25 11:20:28.366808",
+ "modified": "2024-03-11 14:46:30.298184",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 8dfd69f..28d4a5e 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -217,6 +217,7 @@
unrealized_profit_loss_account: DF.Link | None
update_billed_amount_in_purchase_order: DF.Check
update_billed_amount_in_purchase_receipt: DF.Check
+ update_outstanding_for_self: DF.Check
update_stock: DF.Check
use_company_roundoff_cost_center: DF.Check
use_transaction_date_exchange_rate: DF.Check
@@ -829,6 +830,10 @@
)
if grand_total and not self.is_internal_transfer():
+ against_voucher = self.name
+ if self.is_return and self.return_against and not self.update_outstanding_for_self:
+ against_voucher = self.return_against
+
# Did not use base_grand_total to book rounding loss gle
gl_entries.append(
self.get_gl_dict(
@@ -842,7 +847,7 @@
"credit_in_account_currency": base_grand_total
if self.party_account_currency == self.company_currency
else grand_total,
- "against_voucher": self.name,
+ "against_voucher": against_voucher,
"against_voucher_type": self.doctype,
"project": self.project,
"cost_center": self.cost_center,
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 88b28ad..ac14d98 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -25,6 +25,7 @@
"is_consolidated",
"is_return",
"return_against",
+ "update_outstanding_for_self",
"update_billed_amount_in_sales_order",
"update_billed_amount_in_delivery_note",
"is_debit_note",
@@ -2171,6 +2172,14 @@
"fieldtype": "Check",
"label": "Don't Create Loyalty Points",
"no_copy": 1
+ },
+ {
+ "default": "1",
+ "depends_on": "eval: doc.is_return && doc.return_against",
+ "description": "Credit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
+ "fieldname": "update_outstanding_for_self",
+ "fieldtype": "Check",
+ "label": "Update Outstanding for Self"
}
],
"icon": "fa fa-file-text",
@@ -2183,7 +2192,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2024-03-01 09:21:54.201289",
+ "modified": "2024-03-11 14:20:34.874192",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
@@ -2238,4 +2247,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index e2cbf5e..bf50e77 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -220,6 +220,7 @@
unrealized_profit_loss_account: DF.Link | None
update_billed_amount_in_delivery_note: DF.Check
update_billed_amount_in_sales_order: DF.Check
+ update_outstanding_for_self: DF.Check
update_stock: DF.Check
use_company_roundoff_cost_center: DF.Check
write_off_account: DF.Link | None
@@ -1219,6 +1220,10 @@
)
if grand_total and not self.is_internal_transfer():
+ against_voucher = self.name
+ if self.is_return and self.return_against and not self.update_outstanding_for_self:
+ against_voucher = self.return_against
+
# Did not use base_grand_total to book rounding loss gle
gl_entries.append(
self.get_gl_dict(
@@ -1232,7 +1237,7 @@
"debit_in_account_currency": base_grand_total
if self.party_account_currency == self.company_currency
else grand_total,
- "against_voucher": self.name,
+ "against_voucher": against_voucher,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
"project": self.project,
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 6d77ef5..38723e9 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -690,7 +690,12 @@
def get_return_entries(self):
doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice"
- filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
+ filters = {
+ "is_return": 1,
+ "docstatus": 1,
+ "company": self.filters.company,
+ "update_outstanding_for_self": 0,
+ }
or_filters = {}
for party_type in self.party_type:
party_field = scrub(party_type)
diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
index 6ff81be..a0f8af5 100644
--- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
@@ -62,7 +62,7 @@
pe.insert()
pe.submit()
- def create_credit_note(self, docname):
+ def create_credit_note(self, docname, do_not_submit=False):
credit_note = create_sales_invoice(
company=self.company,
customer=self.customer,
@@ -72,6 +72,7 @@
cost_center=self.cost_center,
is_return=1,
return_against=docname,
+ do_not_submit=do_not_submit,
)
return credit_note
@@ -149,7 +150,9 @@
)
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
- self.create_credit_note(si.name)
+ cr_note = self.create_credit_note(si.name, do_not_submit=True)
+ cr_note.update_outstanding_for_self = False
+ cr_note.save().submit()
report = execute(filters)
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
@@ -167,6 +170,68 @@
],
)
+ def test_cr_note_flag_to_update_self(self):
+ filters = {
+ "company": self.company,
+ "report_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ "show_remarks": True,
+ }
+
+ # check invoice grand total and invoiced column's value for 3 payment terms
+ si = self.create_sales_invoice(no_payment_schedule=True)
+ name = si.name
+
+ report = execute(filters)
+
+ expected_data = [100, 100, "No Remarks"]
+
+ self.assertEqual(len(report[1]), 1)
+ row = report[1][0]
+ self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
+
+ # check invoice grand total, invoiced, paid and outstanding column's value after payment
+ self.create_payment_entry(si.name)
+ report = execute(filters)
+
+ expected_data_after_payment = [100, 100, 40, 60]
+ self.assertEqual(len(report[1]), 1)
+ row = report[1][0]
+ self.assertEqual(
+ expected_data_after_payment,
+ [row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
+ )
+
+ # check invoice grand total, invoiced, paid and outstanding column's value after credit note
+ cr_note = self.create_credit_note(si.name, do_not_submit=True)
+ cr_note.posting_date = add_days(today(), 1)
+ cr_note.update_outstanding_for_self = True
+ cr_note.save().submit()
+ report = execute(filters)
+
+ expected_data_after_credit_note = [
+ [100.0, 100.0, 40.0, 0.0, 60.0, self.debit_to],
+ [0, 0, 100.0, 0.0, -100.0, self.debit_to],
+ ]
+ self.assertEqual(len(report[1]), 2)
+ for i in range(2):
+ row = report[1][i - 1]
+ # row = report[1][0]
+ self.assertEqual(
+ expected_data_after_credit_note[i - 1],
+ [
+ row.invoice_grand_total,
+ row.invoiced,
+ row.paid,
+ row.credit_note,
+ row.outstanding,
+ row.party_account,
+ ],
+ )
+
def test_payment_againt_po_in_receivable_report(self):
"""
Payments made against Purchase Order will show up as outstanding amount
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index c543dfc..250f21b 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -218,17 +218,18 @@
)
if self.get("is_return") and self.get("return_against") and not self.get("is_pos"):
- # if self.get("is_return") and self.get("return_against"):
- document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
- frappe.msgprint(
- _(
- "{0} will be treated as a standalone {0}. Post creation use {1} tool to reconcile against {2}."
- ).format(
- document_type,
- get_link_to_form("Payment Reconciliation"),
- get_link_to_form(self.doctype, self.get("return_against")),
+ if self.get("update_outstanding_for_self"):
+ document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
+ frappe.msgprint(
+ _(
+ "We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck '{2}' checkbox. <br><br> Or you can use {3} tool to reconcile against {1} later."
+ ).format(
+ frappe.bold(document_type),
+ get_link_to_form(self.doctype, self.get("return_against")),
+ frappe.bold("Update Outstanding for Self"),
+ get_link_to_form("Payment Reconciliation"),
+ )
)
- )
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 8033289..15dfc36 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -356,6 +356,7 @@
erpnext.patches.v15_0.create_advance_payment_status
erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
+erpnext.patches.v14_0.update_flag_for_return_invoices
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
diff --git a/erpnext/patches/v14_0/update_flag_for_return_invoices.py b/erpnext/patches/v14_0/update_flag_for_return_invoices.py
new file mode 100644
index 0000000..feb43be
--- /dev/null
+++ b/erpnext/patches/v14_0/update_flag_for_return_invoices.py
@@ -0,0 +1,62 @@
+from frappe import qb
+
+
+def execute():
+ # Set "update_outstanding_for_self" flag in Credit/Debit Notes
+ # Fetch Credit/Debit notes that does have 'return_against' but still post ledger entries against themselves.
+
+ gle = qb.DocType("GL Entry")
+
+ # Use hardcoded 'creation' date to isolate Credit/Debit notes created post v14 backport
+ # https://github.com/frappe/erpnext/pull/39497
+ creation_date = "2024-01-25"
+
+ si = qb.DocType("Sales Invoice")
+ if cr_notes := (
+ qb.from_(si)
+ .select(si.name)
+ .where(
+ (si.creation.gte(creation_date))
+ & (si.docstatus == 1)
+ & (si.is_return == True)
+ & (si.return_against.notnull())
+ )
+ .run()
+ ):
+ cr_notes = [x[0] for x in cr_notes]
+ if docs_that_require_update := (
+ qb.from_(gle)
+ .select(gle.voucher_no)
+ .distinct()
+ .where((gle.voucher_no.isin(cr_notes)) & (gle.voucher_no == gle.against_voucher))
+ .run()
+ ):
+ docs_that_require_update = [x[0] for x in docs_that_require_update]
+ qb.update(si).set(si.update_outstanding_for_self, True).where(
+ si.name.isin(docs_that_require_update)
+ ).run()
+
+ pi = qb.DocType("Purchase Invoice")
+ if dr_notes := (
+ qb.from_(pi)
+ .select(pi.name)
+ .where(
+ (pi.creation.gte(creation_date))
+ & (pi.docstatus == 1)
+ & (pi.is_return == True)
+ & (pi.return_against.notnull())
+ )
+ .run()
+ ):
+ dr_notes = [x[0] for x in dr_notes]
+ if docs_that_require_update := (
+ qb.from_(gle)
+ .select(gle.voucher_no)
+ .distinct()
+ .where((gle.voucher_no.isin(dr_notes)) & (gle.voucher_no == gle.against_voucher))
+ .run()
+ ):
+ docs_that_require_update = [x[0] for x in docs_that_require_update]
+ qb.update(pi).set(pi.update_outstanding_for_self, True).where(
+ pi.name.isin(docs_that_require_update)
+ ).run()