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:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)):
|
||||
|
||||
@@ -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
|
||||
|
||||
62
erpnext/patches/v14_0/update_flag_for_return_invoices.py
Normal file
62
erpnext/patches/v14_0/update_flag_for_return_invoices.py
Normal 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()
|
||||
Reference in New Issue
Block a user