Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: checkbox to toggle standalone Credit/Debit note behaviour #40372

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ class PurchaseInvoice(BuyingController):
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
Expand Down Expand Up @@ -829,6 +830,10 @@ def make_supplier_gl_entry(self, gl_entries):
)

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(
Expand All @@ -842,7 +847,7 @@ def make_supplier_gl_entry(self, gl_entries):
"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,
Expand Down
13 changes: 11 additions & 2 deletions erpnext/accounts/doctype/sales_invoice/sales_invoice.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -2238,4 +2247,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
}
7 changes: 6 additions & 1 deletion erpnext/accounts/doctype/sales_invoice/sales_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ class SalesInvoice(SellingController):
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
Expand Down Expand Up @@ -1219,6 +1220,10 @@ def make_customer_gl_entry(self, gl_entries):
)

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(
Expand All @@ -1232,7 +1237,7 @@ def make_customer_gl_entry(self, gl_entries):
"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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,12 @@ def allocate_future_payments(self, row):

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def create_payment_entry(self, docname):
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,
Expand All @@ -72,6 +72,7 @@ def create_credit_note(self, docname):
cost_center=self.cost_center,
is_return=1,
return_against=docname,
do_not_submit=do_not_submit,
)

return credit_note
Expand Down Expand Up @@ -149,7 +150,9 @@ def test_accounts_receivable(self):
)

# 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]
Expand All @@ -167,6 +170,68 @@ def test_accounts_receivable(self):
],
)

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
Expand Down
21 changes: 11 additions & 10 deletions erpnext/controllers/accounts_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,17 +218,18 @@ def validate(self):
)

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)):
Expand Down
1 change: 1 addition & 0 deletions erpnext/patches.txt
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ erpnext.patches.v14_0.update_total_asset_cost_field
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
Expand Down
62 changes: 62 additions & 0 deletions erpnext/patches/v14_0/update_flag_for_return_invoices.py
Original file line number Diff line number Diff line change
@@ -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()
Loading