Merge pull request #40440 from frappe/mergify/bp/version-15/pr-40372

refactor: checkbox to toggle standalone Credit/Debit note behaviour (backport #40372)
This commit is contained in:
ruthra kumar
2024-03-14 14:46:16 +05:30
committed by GitHub
9 changed files with 194 additions and 18 deletions

View File

@@ -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",
@@ -1622,13 +1623,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",

View File

@@ -216,6 +216,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
@@ -828,6 +829,10 @@ class PurchaseInvoice(BuyingController):
)
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(
@@ -841,7 +846,7 @@ class PurchaseInvoice(BuyingController):
"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,

View File

@@ -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",
@@ -2162,6 +2163,14 @@
"fieldname": "update_billed_amount_in_delivery_note",
"fieldtype": "Check",
"label": "Update Billed Amount in Delivery Note"
},
{
"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",
@@ -2174,7 +2183,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2023-11-23 16:56:29.679499",
"modified": "2024-03-11 14:20:34.874192",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
@@ -2229,4 +2238,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -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
@@ -1228,6 +1229,10 @@ class SalesInvoice(SellingController):
)
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(
@@ -1241,7 +1246,7 @@ class SalesInvoice(SellingController):
"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,

View File

@@ -690,7 +690,12 @@ class ReceivablePayableReport(object):
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)

View File

@@ -62,7 +62,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
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 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
cost_center=self.cost_center,
is_return=1,
return_against=docname,
do_not_submit=do_not_submit,
)
return credit_note
@@ -149,7 +150,9 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
)
# 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,82 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
],
)
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, do_not_submit=True)
si.set_posting_time = True
si.posting_date = add_days(today(), -1)
si.save().submit()
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.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, si.name],
[0, 0, 100.0, 0.0, -100.0, cr_note.name],
]
self.assertEqual(len(report[1]), 2)
si_row = [
[
row.invoice_grand_total,
row.invoiced,
row.paid,
row.credit_note,
row.outstanding,
row.voucher_no,
]
for row in report[1]
if row.voucher_no == si.name
][0]
cr_note_row = [
[
row.invoice_grand_total,
row.invoiced,
row.paid,
row.credit_note,
row.outstanding,
row.voucher_no,
]
for row in report[1]
if row.voucher_no == cr_note.name
][0]
self.assertEqual(expected_data_after_credit_note[0], si_row)
self.assertEqual(expected_data_after_credit_note[1], cr_note_row)
def test_payment_againt_po_in_receivable_report(self):
"""
Payments made against Purchase Order will show up as outstanding amount

View File

@@ -218,17 +218,18 @@ class AccountsController(TransactionBase):
)
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", "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", "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)):

View File

@@ -354,6 +354,7 @@ execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency"
erpnext.patches.v14_0.update_total_asset_cost_field
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
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

View File

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