diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json
index 2ec0b7f70c8..4d807b09c33 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json
@@ -109,7 +109,8 @@
"Utility Expenses": {},
"Write Off": {},
"Exchange Gain/Loss": {},
- "Gain/Loss on Asset Disposal": {}
+ "Gain/Loss on Asset Disposal": {},
+ "Impairment": {}
},
"root_type": "Expense"
},
@@ -132,7 +133,8 @@
"Source of Funds (Liabilities)": {
"Capital Account": {
"Reserves and Surplus": {},
- "Shareholders Funds": {}
+ "Shareholders Funds": {},
+ "Revaluation Surplus": {}
},
"Current Liabilities": {
"Accounts Payable": {
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py
index e30ad24a374..5a5e232db8d 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py
@@ -72,6 +72,7 @@ def get():
_("Write Off"): {},
_("Exchange Gain/Loss"): {},
_("Gain/Loss on Asset Disposal"): {},
+ _("Impairment"): {},
},
"root_type": "Expense",
},
@@ -104,6 +105,7 @@ def get():
_("Dividends Paid"): {"account_type": "Equity"},
_("Opening Balance Equity"): {"account_type": "Equity"},
_("Retained Earnings"): {"account_type": "Equity"},
+ _("Revaluation Surplus"): {"account_type": "Equity"},
"root_type": "Equity",
},
}
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
index f1efba8a954..4938e6690e5 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
@@ -28,7 +28,12 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
frm.refresh_fields();
frm.page.clear_indicator();
frm.dashboard.hide_progress();
- frappe.msgprint(__("Opening {0} Invoices created", [frm.doc.invoice_type]));
+
+ if (frm.doc.invoice_type == "Sales") {
+ frappe.msgprint(__("Opening Sales Invoices have been created."));
+ } else {
+ frappe.msgprint(__("Opening Purchase Invoices have been created."));
+ }
},
1500,
data.title
@@ -48,12 +53,19 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
!frm.doc.import_in_progress && frm.trigger("make_dashboard");
frm.page.set_primary_action(__("Create Invoices"), () => {
let btn_primary = frm.page.btn_primary.get(0);
+ let freeze_message;
+ if (frm.doc.invoice_type == "Sales") {
+ freeze_message = __("Creating Sales Invoices ...");
+ } else {
+ freeze_message = __("Creating Purchase Invoices ...");
+ }
+
return frm.call({
doc: frm.doc,
btn: $(btn_primary),
method: "make_invoices",
freeze: 1,
- freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
+ freeze_message: freeze_message,
});
});
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index f368c2a9613..83b43a15987 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -516,6 +516,8 @@ def make_payment_request(**args):
if frappe.db.get_single_value("Accounts Settings", "create_pr_in_draft_status", cache=True):
pr.insert(ignore_permissions=True)
if args.submit_doc:
+ if pr.get("__unsaved"):
+ pr.insert(ignore_permissions=True)
pr.submit()
if args.order_type == "Shopping Cart":
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js
index 0f52a4d998e..d72c4724690 100644
--- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js
@@ -20,6 +20,17 @@ frappe.ui.form.on("Process Payment Reconciliation", {
},
};
});
+
+ frm.set_query("default_advance_account", function (doc) {
+ return {
+ filters: {
+ company: doc.company,
+ is_group: 0,
+ account_type: doc.party_type == "Customer" ? "Receivable" : "Payable",
+ root_type: doc.party_type == "Customer" ? "Liability" : "Asset",
+ },
+ };
+ });
frm.set_query("cost_center", function (doc) {
return {
filters: {
@@ -102,6 +113,7 @@ frappe.ui.form.on("Process Payment Reconciliation", {
company(frm) {
frm.set_value("party", "");
frm.set_value("receivable_payable_account", "");
+ frm.set_value("default_advance_account", "");
},
party_type(frm) {
frm.set_value("party", "");
@@ -109,6 +121,7 @@ frappe.ui.form.on("Process Payment Reconciliation", {
party(frm) {
frm.set_value("receivable_payable_account", "");
+ frm.set_value("default_advance_account", "");
if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) {
return frappe.call({
method: "erpnext.accounts.party.get_party_account",
@@ -116,10 +129,16 @@ frappe.ui.form.on("Process Payment Reconciliation", {
company: frm.doc.company,
party_type: frm.doc.party_type,
party: frm.doc.party,
+ include_advance: 1,
},
callback: (r) => {
if (!r.exc && r.message) {
- frm.set_value("receivable_payable_account", r.message);
+ if (typeof r.message === "string") {
+ frm.set_value("receivable_payable_account", r.message);
+ } else if (Array.isArray(r.message)) {
+ frm.set_value("receivable_payable_account", r.message[0]);
+ frm.set_value("default_advance_account", r.message[1]);
+ }
}
frm.refresh();
},
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json
index 1a1ab4d800e..0511571d754 100644
--- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json
@@ -13,6 +13,7 @@
"column_break_io6c",
"party",
"receivable_payable_account",
+ "default_advance_account",
"filter_section",
"from_invoice_date",
"to_invoice_date",
@@ -141,12 +142,23 @@
{
"fieldname": "section_break_a8yx",
"fieldtype": "Section Break"
+ },
+ {
+ "depends_on": "eval:doc.party",
+ "description": "Only 'Payment Entries' made against this advance account are supported.",
+ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
+ "fieldname": "default_advance_account",
+ "fieldtype": "Link",
+ "label": "Default Advance Account",
+ "mandatory_depends_on": "doc.party_type",
+ "options": "Account",
+ "reqd": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-08-11 10:56:51.699137",
+ "modified": "2024-08-27 14:48:56.715320",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation",
@@ -180,4 +192,4 @@
"sort_order": "DESC",
"states": [],
"title_field": "company"
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
index 5048fc5e25e..882a2c4ad1c 100644
--- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
@@ -23,6 +23,7 @@ class ProcessPaymentReconciliation(Document):
bank_cash_account: DF.Link | None
company: DF.Link
cost_center: DF.Link | None
+ default_advance_account: DF.Link
error_log: DF.LongText | None
from_invoice_date: DF.Date | None
from_payment_date: DF.Date | None
@@ -101,6 +102,7 @@ def get_pr_instance(doc: str):
"party_type",
"party",
"receivable_payable_account",
+ "default_advance_account",
"from_invoice_date",
"to_invoice_date",
"from_payment_date",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index de99c35e27e..623fb941b89 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -285,7 +285,6 @@ class PurchaseInvoice(BuyingController):
self.set_against_expense_account()
self.validate_write_off_account()
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
- self.create_remarks()
self.set_status()
self.validate_purchase_receipt_if_update_stock()
validate_inter_company_party(
@@ -322,10 +321,11 @@ class PurchaseInvoice(BuyingController):
def create_remarks(self):
if not self.remarks:
- if self.bill_no and self.bill_date:
- self.remarks = _("Against Supplier Invoice {0} dated {1}").format(
- self.bill_no, formatdate(self.bill_date)
- )
+ if self.bill_no:
+ self.remarks = _("Against Supplier Invoice {0}").format(self.bill_no)
+ if self.bill_date:
+ self.remarks += " " + _("dated {0}").format(formatdate(self.bill_date))
+
else:
self.remarks = _("No Remarks")
@@ -747,6 +747,9 @@ class PurchaseInvoice(BuyingController):
validate_docs_for_voucher_types(["Purchase Invoice"])
validate_docs_for_deferred_accounting([], [self.name])
+ def before_submit(self):
+ self.create_remarks()
+
def on_submit(self):
super().on_submit()
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 1966a78ef49..12e26623ad0 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -278,7 +278,6 @@ class SalesInvoice(SellingController):
self.check_sales_order_on_hold_or_close("sales_order")
self.validate_debit_to_acc()
self.clear_unallocated_advances("Sales Invoice Advance", "advances")
- self.add_remarks()
self.validate_fixed_asset()
self.set_income_account_for_fixed_assets()
self.validate_item_cost_centers()
@@ -422,6 +421,9 @@ class SalesInvoice(SellingController):
self.set_account_for_mode_of_payment()
self.set_paid_amount()
+ def before_submit(self):
+ self.add_remarks()
+
def on_submit(self):
self.validate_pos_paid_amount()
@@ -946,10 +948,11 @@ class SalesInvoice(SellingController):
def add_remarks(self):
if not self.remarks:
- if self.po_no and self.po_date:
- self.remarks = _("Against Customer Order {0} dated {1}").format(
- self.po_no, formatdate(self.po_date)
- )
+ if self.po_no:
+ self.remarks = _("Against Customer Order {0}").format(self.po_no)
+ if self.po_date:
+ self.remarks += " " + _("dated {0}").format(formatdate(self.po_data))
+
else:
self.remarks = _("No Remarks")
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index fef30bdfecd..beb347e07db 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -3162,6 +3162,50 @@ class TestSalesInvoice(FrappeTestCase):
party_link.delete()
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
+ def test_sales_invoice_cancel_with_common_party_advance_jv(self):
+ from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
+ make_customer,
+ )
+ from erpnext.accounts.doctype.party_link.party_link import create_party_link
+ from erpnext.buying.doctype.supplier.test_supplier import create_supplier
+
+ # create a customer
+ customer = make_customer(customer="_Test Common Supplier")
+ # create a supplier
+ supplier = create_supplier(supplier_name="_Test Common Supplier").name
+
+ # create a party link between customer & supplier
+ party_link = create_party_link("Supplier", supplier, customer)
+
+ # enable common party accounting
+ frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
+
+ # create a sales invoice
+ si = create_sales_invoice(customer=customer)
+
+ # check creation of journal entry
+ jv = frappe.db.get_value(
+ "Journal Entry Account",
+ filters={
+ "reference_type": si.doctype,
+ "reference_name": si.name,
+ "docstatus": 1,
+ },
+ fieldname="parent",
+ )
+
+ self.assertTrue(jv)
+
+ # cancel sales invoice
+ si.cancel()
+
+ # check cancellation of journal entry
+ jv_status = frappe.db.get_value("Journal Entry", jv, "docstatus")
+ self.assertEqual(jv_status, 2)
+
+ party_link.delete()
+ frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
+
def test_payment_statuses(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
@@ -3871,6 +3915,88 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(len(res), 1)
self.assertEqual(res[0][0], pos_return.return_against)
+ @change_settings("Accounts Settings", {"enable_common_party_accounting": True})
+ def test_common_party_with_foreign_currency_jv(self):
+ from erpnext.accounts.doctype.account.test_account import create_account
+ from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
+ make_customer,
+ )
+ from erpnext.accounts.doctype.party_link.party_link import create_party_link
+ from erpnext.buying.doctype.supplier.test_supplier import create_supplier
+ from erpnext.setup.utils import get_exchange_rate
+
+ creditors = create_account(
+ account_name="Creditors USD",
+ parent_account="Accounts Payable - _TC",
+ company="_Test Company",
+ account_currency="USD",
+ account_type="Payable",
+ )
+ debtors = create_account(
+ account_name="Debtors USD",
+ parent_account="Accounts Receivable - _TC",
+ company="_Test Company",
+ account_currency="USD",
+ account_type="Receivable",
+ )
+
+ # create a customer
+ customer = make_customer(customer="_Test Common Party USD")
+ cust_doc = frappe.get_doc("Customer", customer)
+ cust_doc.default_currency = "USD"
+ test_account_details = {
+ "company": "_Test Company",
+ "account": debtors,
+ }
+ cust_doc.append("accounts", test_account_details)
+ cust_doc.save()
+
+ # create a supplier
+ supplier = create_supplier(supplier_name="_Test Common Party USD").name
+ supp_doc = frappe.get_doc("Supplier", supplier)
+ supp_doc.default_currency = "USD"
+ test_account_details = {
+ "company": "_Test Company",
+ "account": creditors,
+ }
+ supp_doc.append("accounts", test_account_details)
+ supp_doc.save()
+
+ # create a party link between customer & supplier
+ create_party_link("Supplier", supplier, customer)
+
+ # create a sales invoice
+ si = create_sales_invoice(
+ customer=customer,
+ currency="USD",
+ conversion_rate=get_exchange_rate("USD", "INR"),
+ debit_to=debtors,
+ do_not_save=1,
+ )
+ si.party_account_currency = "USD"
+ si.save()
+ si.submit()
+
+ # check outstanding of sales invoice
+ si.reload()
+ self.assertEqual(si.status, "Paid")
+ self.assertEqual(flt(si.outstanding_amount), 0.0)
+
+ # check creation of journal entry
+ jv = frappe.get_all(
+ "Journal Entry Account",
+ {
+ "account": si.debit_to,
+ "party_type": "Customer",
+ "party": si.customer,
+ "reference_type": si.doctype,
+ "reference_name": si.name,
+ },
+ pluck="credit_in_account_currency",
+ )
+ self.assertTrue(jv)
+ self.assertEqual(jv[0], si.grand_total)
+
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index e6015e081d6..06e285e4ada 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -68,7 +68,7 @@ def get_party_details(
pos_profile=None,
):
if not party:
- return {}
+ return frappe._dict()
if not frappe.db.exists(party_type, party):
frappe.throw(_("{0}: {1} does not exists").format(party_type, party))
return _get_party_details(
diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js
index 86463f1f1f2..445e532183b 100644
--- a/erpnext/accounts/report/accounts_payable/accounts_payable.js
+++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js
@@ -61,32 +61,10 @@ frappe.query_reports["Accounts Payable"] = {
default: "Due Date",
},
{
- fieldname: "range1",
- label: __("Ageing Range 1"),
- fieldtype: "Int",
- default: "30",
- reqd: 1,
- },
- {
- fieldname: "range2",
- label: __("Ageing Range 2"),
- fieldtype: "Int",
- default: "60",
- reqd: 1,
- },
- {
- fieldname: "range3",
- label: __("Ageing Range 3"),
- fieldtype: "Int",
- default: "90",
- reqd: 1,
- },
- {
- fieldname: "range4",
- label: __("Ageing Range 4"),
- fieldtype: "Int",
- default: "120",
- reqd: 1,
+ fieldname: "range",
+ label: __("Ageing Range"),
+ fieldtype: "Data",
+ default: "30, 60, 90, 120",
},
{
fieldname: "payment_terms_template",
@@ -162,6 +140,11 @@ frappe.query_reports["Accounts Payable"] = {
label: __("In Party Currency"),
fieldtype: "Check",
},
+ {
+ fieldname: "handle_employee_advances",
+ label: __("Handle Employee Advances"),
+ fieldtype: "Check",
+ },
],
formatter: function (value, row, column, data, default_formatter) {
diff --git a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py
index 43856bf569f..8971dc3d37b 100644
--- a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py
+++ b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py
@@ -30,10 +30,7 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
"party_type": "Supplier",
"party": [self.supplier],
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
"in_party_currency": 1,
}
diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js
index 92ea9e8f598..cf7a62c6b69 100644
--- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js
+++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js
@@ -24,32 +24,10 @@ frappe.query_reports["Accounts Payable Summary"] = {
default: "Due Date",
},
{
- fieldname: "range1",
- label: __("Ageing Range 1"),
- fieldtype: "Int",
- default: "30",
- reqd: 1,
- },
- {
- fieldname: "range2",
- label: __("Ageing Range 2"),
- fieldtype: "Int",
- default: "60",
- reqd: 1,
- },
- {
- fieldname: "range3",
- label: __("Ageing Range 3"),
- fieldtype: "Int",
- default: "90",
- reqd: 1,
- },
- {
- fieldname: "range4",
- label: __("Ageing Range 4"),
- fieldtype: "Int",
- default: "120",
- reqd: 1,
+ fieldname: "range",
+ label: __("Ageing Range"),
+ fieldtype: "Data",
+ default: "30, 60, 90, 120",
},
{
fieldname: "finance_book",
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
index 7e795fbe3c1..9f15bbc333d 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
@@ -89,32 +89,10 @@ frappe.query_reports["Accounts Receivable"] = {
default: "Due Date",
},
{
- fieldname: "range1",
- label: __("Ageing Range 1"),
- fieldtype: "Int",
- default: "30",
- reqd: 1,
- },
- {
- fieldname: "range2",
- label: __("Ageing Range 2"),
- fieldtype: "Int",
- default: "60",
- reqd: 1,
- },
- {
- fieldname: "range3",
- label: __("Ageing Range 3"),
- fieldtype: "Int",
- default: "90",
- reqd: 1,
- },
- {
- fieldname: "range4",
- label: __("Ageing Range 4"),
- fieldtype: "Int",
- default: "120",
- reqd: 1,
+ fieldname: "range",
+ label: __("Ageing Range"),
+ fieldtype: "Data",
+ default: "30, 60, 90, 120",
},
{
fieldname: "customer_group",
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index f8511d2f497..1a7638aaae4 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -50,6 +50,11 @@ class ReceivablePayableReport:
getdate(nowdate()) if self.filters.report_date > getdate(nowdate()) else self.filters.report_date
)
+ if not self.filters.range:
+ self.filters.range = "30, 60, 90, 120"
+ self.ranges = [num.strip() for num in self.filters.range.split(",") if num.strip().isdigit()]
+ self.range_numbers = [num for num in range(1, len(self.ranges) + 2)]
+
def run(self, args):
self.filters.update(args)
self.set_defaults()
@@ -112,6 +117,26 @@ class ReceivablePayableReport:
self.build_data()
+ def build_voucher_dict(self, ple):
+ return frappe._dict(
+ voucher_type=ple.voucher_type,
+ voucher_no=ple.voucher_no,
+ party=ple.party,
+ party_account=ple.account,
+ posting_date=ple.posting_date,
+ account_currency=ple.account_currency,
+ remarks=ple.remarks,
+ invoiced=0.0,
+ paid=0.0,
+ credit_note=0.0,
+ outstanding=0.0,
+ invoiced_in_account_currency=0.0,
+ paid_in_account_currency=0.0,
+ credit_note_in_account_currency=0.0,
+ outstanding_in_account_currency=0.0,
+ cost_center=ple.cost_center,
+ )
+
def init_voucher_balance(self):
# build all keys, since we want to exclude vouchers beyond the report date
for ple in self.ple_entries:
@@ -123,24 +148,8 @@ class ReceivablePayableReport:
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
if key not in self.voucher_balance:
- self.voucher_balance[key] = frappe._dict(
- voucher_type=ple.voucher_type,
- voucher_no=ple.voucher_no,
- party=ple.party,
- party_account=ple.account,
- posting_date=ple.posting_date,
- account_currency=ple.account_currency,
- remarks=ple.remarks,
- invoiced=0.0,
- paid=0.0,
- credit_note=0.0,
- outstanding=0.0,
- invoiced_in_account_currency=0.0,
- paid_in_account_currency=0.0,
- credit_note_in_account_currency=0.0,
- outstanding_in_account_currency=0.0,
- cost_center=ple.cost_center,
- )
+ self.voucher_balance[key] = self.build_voucher_dict(ple)
+
self.get_invoices(ple)
if self.filters.get("group_by_party"):
@@ -208,6 +217,18 @@ class ReceivablePayableReport:
row = self.voucher_balance.get(key)
+ # Build and use a separate row for Employee Advances.
+ # This allows Payments or Journals made against Emp Advance to be processed.
+ if (
+ not row
+ and ple.against_voucher_type == "Employee Advance"
+ and self.filters.handle_employee_advances
+ ):
+ _d = self.build_voucher_dict(ple)
+ _d.voucher_type = ple.against_voucher_type
+ _d.voucher_no = ple.against_voucher_no
+ row = self.voucher_balance[key] = _d
+
if not row:
# no invoice, this is an invoice / stand-alone payment / credit note
if self.filters.get("ignore_accounts"):
@@ -717,37 +738,22 @@ class ReceivablePayableReport:
# ageing buckets should not have amounts if due date is not reached
if getdate(entry_date) > getdate(self.filters.report_date):
- row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0
+ [setattr(row, f"range{i}", 0.0) for i in self.range_numbers]
- row.total_due = row.range1 + row.range2 + row.range3 + row.range4 + row.range5
+ row.total_due = sum(row[f"range{i}"] for i in self.range_numbers)
def get_ageing_data(self, entry_date, row):
# [0-30, 30-60, 60-90, 90-120, 120-above]
- row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0
+ [setattr(row, f"range{i}", 0.0) for i in self.range_numbers]
if not (self.age_as_on and entry_date):
return
row.age = (getdate(self.age_as_on) - getdate(entry_date)).days or 0
- index = None
- if not (self.filters.range1 and self.filters.range2 and self.filters.range3 and self.filters.range4):
- self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4 = (
- 30,
- 60,
- 90,
- 120,
- )
-
- for i, days in enumerate(
- [self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4]
- ):
- if cint(row.age) <= cint(days):
- index = i
- break
-
- if index is None:
- index = 4
+ index = next(
+ (i for i, days in enumerate(self.ranges) if cint(row.age) <= cint(days)), len(self.ranges)
+ )
row["range" + str(index + 1)] = row.outstanding
def get_ple_entries(self):
@@ -1059,6 +1065,7 @@ class ReceivablePayableReport:
self.add_column(_("Debit Note"), fieldname="credit_note")
self.add_column(_("Outstanding Amount"), fieldname="outstanding")
+ self.add_column(label=_("Age (Days)"), fieldname="age", fieldtype="Int", width=80)
self.setup_ageing_columns()
self.add_column(
@@ -1117,34 +1124,26 @@ class ReceivablePayableReport:
def setup_ageing_columns(self):
# for charts
self.ageing_column_labels = []
- self.add_column(label=_("Age (Days)"), fieldname="age", fieldtype="Int", width=80)
+ ranges = [*self.ranges, "Above"]
+
+ prev_range_value = 0
+ for idx, curr_range_value in enumerate(ranges):
+ label = f"{prev_range_value}-{curr_range_value}"
+ self.add_column(label=label, fieldname="range" + str(idx + 1))
- for i, label in enumerate(
- [
- "0-{range1}".format(range1=self.filters["range1"]),
- "{range1}-{range2}".format(
- range1=cint(self.filters["range1"]) + 1, range2=self.filters["range2"]
- ),
- "{range2}-{range3}".format(
- range2=cint(self.filters["range2"]) + 1, range3=self.filters["range3"]
- ),
- "{range3}-{range4}".format(
- range3=cint(self.filters["range3"]) + 1, range4=self.filters["range4"]
- ),
- _("{range4}-Above").format(range4=cint(self.filters["range4"]) + 1),
- ]
- ):
- self.add_column(label=label, fieldname="range" + str(i + 1))
self.ageing_column_labels.append(label)
+ if curr_range_value.isdigit():
+ prev_range_value = cint(curr_range_value) + 1
+
def get_chart_data(self):
+ precision = cint(frappe.db.get_default("float_precision")) or 2
rows = []
for row in self.data:
row = frappe._dict(row)
if not cint(row.bold):
- values = [row.range1, row.range2, row.range3, row.range4, row.range5]
- precision = cint(frappe.db.get_default("float_precision")) or 2
- rows.append({"values": [flt(val, precision) for val in values]})
+ values = [flt(row.get(f"range{i}", None), precision) for i in self.range_numbers]
+ rows.append({"values": values})
self.chart = {
"data": {"labels": self.ageing_column_labels, "datasets": rows},
diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
index c4baa4e4842..39ca78153c3 100644
--- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
@@ -83,10 +83,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"party": [self.customer],
"report_date": add_days(today(), 2),
"based_on_payment_terms": 0,
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
"show_remarks": False,
}
@@ -116,10 +113,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"company": self.company,
"based_on_payment_terms": 1,
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
"show_remarks": True,
}
@@ -172,10 +166,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
"show_remarks": True,
}
@@ -266,10 +257,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"company": self.company,
"based_on_payment_terms": 0,
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
}
report = execute(filters)
@@ -328,10 +316,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
}
report = execute(filters)
@@ -397,10 +382,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
}
report = execute(filters)
self.assertEqual(report[1], [])
@@ -416,10 +398,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
"group_by_party": True,
}
report = execute(filters)[1]
@@ -493,10 +472,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
"show_future_payments": True,
}
report = execute(filters)[1]
@@ -555,10 +531,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
"sales_person": sales_person.name,
"show_sales_person": True,
}
@@ -575,10 +548,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
"cost_center": self.cost_center,
}
report = execute(filters)[1]
@@ -593,10 +563,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
"customer_group": cus_group,
}
report = execute(filters)[1]
@@ -618,10 +585,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
"customer_group": cus_groups_list, # Use the list of customer groups
}
report = execute(filters)[1]
@@ -660,10 +624,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
"party_account": self.debit_to,
}
report = execute(filters)[1]
@@ -711,10 +672,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"party_type": "Customer",
"party": [self.customer],
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
"in_party_currency": 1,
}
@@ -754,10 +712,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"party_type": "Customer",
"party": [self.customer1, self.customer3],
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
}
si1 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
@@ -837,10 +792,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
}
report_ouput = execute(filters)[1]
@@ -903,10 +855,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
{
"company": self.company,
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
"show_future_payments": True,
"in_party_currency": False,
}
@@ -965,10 +914,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
}
# check invoice grand total and invoiced column's value for 3 payment terms
@@ -991,10 +937,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
}
# check invoice grand total and invoiced column's value for 3 payment terms
diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js
index 964abc23747..e36f40169b3 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js
+++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js
@@ -24,32 +24,10 @@ frappe.query_reports["Accounts Receivable Summary"] = {
default: "Due Date",
},
{
- fieldname: "range1",
- label: __("Ageing Range 1"),
- fieldtype: "Int",
- default: "30",
- reqd: 1,
- },
- {
- fieldname: "range2",
- label: __("Ageing Range 2"),
- fieldtype: "Int",
- default: "60",
- reqd: 1,
- },
- {
- fieldname: "range3",
- label: __("Ageing Range 3"),
- fieldtype: "Int",
- default: "90",
- reqd: 1,
- },
- {
- fieldname: "range4",
- label: __("Ageing Range 4"),
- fieldtype: "Int",
- default: "120",
- reqd: 1,
+ fieldname: "range",
+ label: __("Ageing Range"),
+ fieldtype: "Data",
+ default: "30, 60, 90, 120",
},
{
fieldname: "finance_book",
diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
index 6a1b1057724..87fc7ea68be 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
+++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
@@ -104,25 +104,23 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.set_party_details(d)
def init_party_total(self, row):
+ default_dict = {
+ "invoiced": 0.0,
+ "paid": 0.0,
+ "credit_note": 0.0,
+ "outstanding": 0.0,
+ "total_due": 0.0,
+ "future_amount": 0.0,
+ "sales_person": [],
+ "party_type": row.party_type,
+ }
+ for i in self.range_numbers:
+ range_key = f"range{i}"
+ default_dict[range_key] = 0.0
+
self.party_total.setdefault(
row.party,
- frappe._dict(
- {
- "invoiced": 0.0,
- "paid": 0.0,
- "credit_note": 0.0,
- "outstanding": 0.0,
- "range1": 0.0,
- "range2": 0.0,
- "range3": 0.0,
- "range4": 0.0,
- "range5": 0.0,
- "total_due": 0.0,
- "future_amount": 0.0,
- "sales_person": [],
- "party_type": row.party_type,
- }
- ),
+ frappe._dict(default_dict),
)
def set_party_details(self, row):
@@ -173,6 +171,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.add_column(_("Difference"), fieldname="diff")
self.setup_ageing_columns()
+ self.add_column(label="Total Amount Due", fieldname="total_due")
if self.filters.show_future_payments:
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
@@ -206,27 +205,6 @@ class AccountsReceivableSummary(ReceivablePayableReport):
label=_("Currency"), fieldname="currency", fieldtype="Link", options="Currency", width=80
)
- def setup_ageing_columns(self):
- for i, label in enumerate(
- [
- "0-{range1}".format(range1=self.filters["range1"]),
- "{range1}-{range2}".format(
- range1=cint(self.filters["range1"]) + 1, range2=self.filters["range2"]
- ),
- "{range2}-{range3}".format(
- range2=cint(self.filters["range2"]) + 1, range3=self.filters["range3"]
- ),
- "{range3}-{range4}".format(
- range3=cint(self.filters["range3"]) + 1, range4=self.filters["range4"]
- ),
- "{range4}-{above}".format(range4=cint(self.filters["range4"]) + 1, above=_("Above")),
- ]
- ):
- self.add_column(label=label, fieldname="range" + str(i + 1))
-
- # Add column for total due amount
- self.add_column(label="Total Amount Due", fieldname="total_due")
-
def get_gl_balance(report_date, company):
return frappe._dict(
diff --git a/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py
index 4ef607bab28..a98cc6af7a3 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py
+++ b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py
@@ -27,10 +27,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"company": self.company,
"customer": self.customer,
"posting_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
}
si = create_sales_invoice(
@@ -121,10 +118,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"company": self.company,
"customer": self.customer,
"posting_date": today(),
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
+ "range": "30, 60, 90, 120",
}
si = create_sales_invoice(
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html
index 3c4e1a05c97..bdea568bdf4 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.html
+++ b/erpnext/accounts/report/general_ledger/general_ledger.html
@@ -48,8 +48,9 @@
{% } %}
-
{%= __("Remarks") %}: {%= data[i].remarks %}
- {% if(data[i].bill_no) { %}
+ {% if(data[i].remarks) { %}
+
{%= __("Remarks") %}: {%= data[i].remarks %}
+ {% } else if(data[i].bill_no) { %}
{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
{% } %}
diff --git a/erpnext/accounts/report/invalid_ledger_entries/__init__.py b/erpnext/accounts/report/invalid_ledger_entries/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js
new file mode 100644
index 00000000000..47d478f2865
--- /dev/null
+++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js
@@ -0,0 +1,51 @@
+// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+function get_filters() {
+ let filters = [
+ {
+ fieldname: "company",
+ label: __("Company"),
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1,
+ },
+ {
+ fieldname: "from_date",
+ label: __("Start Date"),
+ fieldtype: "Date",
+ reqd: 1,
+ default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ },
+ {
+ fieldname: "to_date",
+ label: __("End Date"),
+ fieldtype: "Date",
+ reqd: 1,
+ default: frappe.datetime.get_today(),
+ },
+ {
+ fieldname: "account",
+ label: __("Account"),
+ fieldtype: "MultiSelectList",
+ options: "Account",
+ get_data: function (txt) {
+ return frappe.db.get_link_options("Account", txt, {
+ company: frappe.query_report.get_filter_value("company"),
+ });
+ },
+ },
+ {
+ fieldname: "voucher_no",
+ label: __("Voucher No"),
+ fieldtype: "Data",
+ width: 100,
+ },
+ ];
+ return filters;
+}
+
+frappe.query_reports["Invalid Ledger Entries"] = {
+ filters: get_filters(),
+};
diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.json b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.json
new file mode 100644
index 00000000000..00dbbfc5056
--- /dev/null
+++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.json
@@ -0,0 +1,23 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2024-09-09 12:31:25.295976",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letterhead": null,
+ "modified": "2024-09-09 12:31:25.295976",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Invalid Ledger Entries",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "GL Entry",
+ "report_name": "Invalid Ledger Entries",
+ "report_type": "Script Report",
+ "roles": [],
+ "timeout": 0
+}
\ No newline at end of file
diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py
new file mode 100644
index 00000000000..33fda705cf2
--- /dev/null
+++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py
@@ -0,0 +1,137 @@
+# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _, qb
+from frappe.query_builder import Criterion
+from frappe.query_builder.custom import ConstantColumn
+
+
+def execute(filters: dict | None = None):
+ """Return columns and data for the report.
+
+ This is the main entry point for the report. It accepts the filters as a
+ dictionary and should return columns and data. It is called by the framework
+ every time the report is refreshed or a filter is updated.
+ """
+ validate_filters(filters)
+
+ columns = get_columns()
+ data = get_data(filters)
+
+ return columns, data
+
+
+def get_columns() -> list[dict]:
+ """Return columns for the report.
+
+ One field definition per column, just like a DocType field definition.
+ """
+ return [
+ {"label": _("Voucher Type"), "fieldname": "voucher_type", "fieldtype": "Link", "options": "DocType"},
+ {
+ "label": _("Voucher No"),
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "options": "voucher_type",
+ },
+ ]
+
+
+def get_data(filters) -> list[list]:
+ """Return data for the report.
+
+ The report data is a list of rows, with each row being a list of cell values.
+ """
+ active_vouchers = get_active_vouchers_for_period(filters)
+ invalid_vouchers = identify_cancelled_vouchers(active_vouchers)
+
+ return invalid_vouchers
+
+
+def identify_cancelled_vouchers(active_vouchers: list[dict] | list | None = None) -> list[dict]:
+ cancelled_vouchers = []
+ if active_vouchers:
+ # Group by voucher types and use single query to identify cancelled vouchers
+ vtypes = set([x.voucher_type for x in active_vouchers])
+
+ for _t in vtypes:
+ _names = [x.voucher_no for x in active_vouchers if x.voucher_type == _t]
+ dt = qb.DocType(_t)
+ non_active_vouchers = (
+ qb.from_(dt)
+ .select(ConstantColumn(_t).as_("voucher_type"), dt.name.as_("voucher_no"))
+ .where(dt.docstatus.ne(1) & dt.name.isin(_names))
+ .run(as_dict=True)
+ )
+ if non_active_vouchers:
+ cancelled_vouchers.extend(non_active_vouchers)
+ return cancelled_vouchers
+
+
+def validate_filters(filters: dict | None = None):
+ if not filters:
+ frappe.throw(_("Filters missing"))
+
+ if not filters.company:
+ frappe.throw(_("Company is mandatory"))
+
+ if filters.from_date > filters.to_date:
+ frappe.throw(_("Start Date should be lower than End Date"))
+
+
+def build_query_filters(filters: dict | None = None) -> list:
+ qb_filters = []
+ if filters:
+ if filters.account:
+ qb_filters.append(qb.Field("account").isin(filters.account))
+
+ if filters.voucher_no:
+ qb_filters.append(qb.Field("voucher_no").eq(filters.voucher_no))
+
+ return qb_filters
+
+
+def get_active_vouchers_for_period(filters: dict | None = None) -> list[dict]:
+ uniq_vouchers = []
+
+ if filters:
+ gle = qb.DocType("GL Entry")
+ ple = qb.DocType("Payment Ledger Entry")
+
+ qb_filters = build_query_filters(filters)
+
+ gl_vouchers = (
+ qb.from_(gle)
+ .select(gle.voucher_type)
+ .distinct()
+ .select(gle.voucher_no)
+ .distinct()
+ .where(
+ gle.is_cancelled.eq(0)
+ & gle.company.eq(filters.company)
+ & gle.posting_date[filters.from_date : filters.to_date]
+ )
+ .where(Criterion.all(qb_filters))
+ .run(as_dict=True)
+ )
+
+ pl_vouchers = (
+ qb.from_(ple)
+ .select(ple.voucher_type)
+ .distinct()
+ .select(ple.voucher_no)
+ .distinct()
+ .where(
+ ple.delinked.eq(0)
+ & ple.company.eq(filters.company)
+ & ple.posting_date[filters.from_date : filters.to_date]
+ )
+ .where(Criterion.all(qb_filters))
+ .run(as_dict=True)
+ )
+
+ uniq_vouchers.extend(gl_vouchers)
+ uniq_vouchers.extend(pl_vouchers)
+
+ return uniq_vouchers
diff --git a/erpnext/accounts/test/test_reports.py b/erpnext/accounts/test/test_reports.py
index c2e10f8fd47..56b7832a32e 100644
--- a/erpnext/accounts/test/test_reports.py
+++ b/erpnext/accounts/test/test_reports.py
@@ -14,8 +14,8 @@ DEFAULT_FILTERS = {
REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
("General Ledger", {"group_by": "Group by Voucher (Consolidated)"}),
("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1}),
- ("Accounts Payable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
- ("Accounts Receivable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
+ ("Accounts Payable", {"range": "30, 60, 90, 120"}),
+ ("Accounts Receivable", {"range": "30, 60, 90, 120"}),
("Consolidated Financial Statement", {"report": "Balance Sheet"}),
("Consolidated Financial Statement", {"report": "Profit and Loss Statement"}),
("Consolidated Financial Statement", {"report": "Cash Flow"}),
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 0450221222d..e462f749b54 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -781,6 +781,46 @@ def cancel_exchange_gain_loss_journal(
gain_loss_je.cancel()
+def cancel_common_party_journal(self):
+ if self.doctype not in ["Sales Invoice", "Purchase Invoice"]:
+ return
+
+ if not frappe.db.get_single_value("Accounts Settings", "enable_common_party_accounting"):
+ return
+
+ party_link = self.get_common_party_link()
+ if not party_link:
+ return
+
+ journal_entry = frappe.db.get_value(
+ "Journal Entry Account",
+ filters={
+ "reference_type": self.doctype,
+ "reference_name": self.name,
+ "docstatus": 1,
+ },
+ fieldname="parent",
+ )
+
+ if not journal_entry:
+ return
+
+ common_party_journal = frappe.db.get_value(
+ "Journal Entry",
+ filters={
+ "name": journal_entry,
+ "is_system_generated": True,
+ "docstatus": 1,
+ },
+ )
+
+ if not common_party_journal:
+ return
+
+ common_party_je = frappe.get_doc("Journal Entry", common_party_journal)
+ common_party_je.cancel()
+
+
def update_accounting_ledgers_after_reference_removal(
ref_type: str | None = None, ref_no: str | None = None, payment_name: str | None = None
):
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 2477e443c4a..0a21516c177 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -506,6 +506,7 @@ frappe.ui.form.on("Asset", {
create_asset_repair: function (frm) {
frappe.call({
args: {
+ company: frm.doc.company,
asset: frm.doc.name,
asset_name: frm.doc.asset_name,
},
@@ -520,6 +521,7 @@ frappe.ui.form.on("Asset", {
create_asset_capitalization: function (frm) {
frappe.call({
args: {
+ company: frm.doc.company,
asset: frm.doc.name,
asset_name: frm.doc.asset_name,
item_code: frm.doc.item_code,
@@ -528,6 +530,7 @@ frappe.ui.form.on("Asset", {
callback: function (r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
+ $(".primary-action").prop("hidden", false);
},
});
},
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index f6846ea3cee..58f813247f0 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -125,7 +125,6 @@ class Asset(AccountsController):
self.validate_cost_center()
self.set_missing_values()
self.validate_gross_and_purchase_amount()
- self.validate_expected_value_after_useful_life()
self.validate_finance_books()
if not self.split_from:
@@ -146,6 +145,7 @@ class Asset(AccountsController):
"Asset Depreciation Schedules created:
{0}
Please check, edit if needed, and submit the Asset."
).format(asset_depr_schedules_links)
)
+ self.validate_expected_value_after_useful_life()
self.set_total_booked_depreciations()
self.total_asset_cost = self.gross_purchase_amount
self.status = self.get_status()
@@ -895,18 +895,19 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan
@frappe.whitelist()
-def create_asset_repair(asset, asset_name):
+def create_asset_repair(company, asset, asset_name):
asset_repair = frappe.new_doc("Asset Repair")
- asset_repair.update({"asset": asset, "asset_name": asset_name})
+ asset_repair.update({"company": company, "asset": asset, "asset_name": asset_name})
return asset_repair
@frappe.whitelist()
-def create_asset_capitalization(asset, asset_name, item_code):
+def create_asset_capitalization(company, asset, asset_name, item_code):
asset_capitalization = frappe.new_doc("Asset Capitalization")
asset_capitalization.update(
{
"target_asset": asset,
+ "company": company,
"capitalization_method": "Choose a WIP composite asset",
"target_asset_name": asset_name,
"target_item_code": item_code,
diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py
index a7e03c08fac..bd0798236b3 100644
--- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py
+++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py
@@ -175,7 +175,7 @@ def get_data(filters):
"purchase_order": po.parent,
"supplier": po.supplier,
"estimated_cost": flt(mr_record.get("amount")),
- "actual_cost": flt(pi_records.get(po.name)),
+ "actual_cost": flt(pi_records.get(po.name)) or flt(po.amount),
"purchase_order_amt": flt(po.amount),
"purchase_order_amt_in_company_currency": flt(po.base_amount),
"expected_delivery_date": po.schedule_date,
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index ce5d813b801..49198be9b48 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1580,6 +1580,7 @@ class AccountsController(TransactionBase):
remove_from_bank_transaction,
)
from erpnext.accounts.utils import (
+ cancel_common_party_journal,
cancel_exchange_gain_loss_journal,
unlink_ref_doc_from_payment_entries,
)
@@ -1591,6 +1592,7 @@ class AccountsController(TransactionBase):
# Cancel Exchange Gain/Loss Journal before unlinking
cancel_exchange_gain_loss_journal(self)
+ cancel_common_party_journal(self)
if frappe.db.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"):
unlink_ref_doc_from_payment_entries(self)
@@ -2418,12 +2420,15 @@ class AccountsController(TransactionBase):
primary_account = get_party_account(primary_party_type, primary_party, self.company)
secondary_account = get_party_account(secondary_party_type, secondary_party, self.company)
+ primary_account_currency = get_account_currency(primary_account)
+ secondary_account_currency = get_account_currency(secondary_account)
jv = frappe.new_doc("Journal Entry")
jv.voucher_type = "Journal Entry"
jv.posting_date = self.posting_date
jv.company = self.company
jv.remark = f"Adjustment for {self.doctype} {self.name}"
+ jv.is_system_generated = True
reconcilation_entry = frappe._dict()
advance_entry = frappe._dict()
@@ -2457,6 +2462,10 @@ class AccountsController(TransactionBase):
advance_entry.credit_in_account_currency = self.outstanding_amount
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
+ default_currency = erpnext.get_company_currency(self.company)
+ if primary_account_currency != default_currency or secondary_account_currency != default_currency:
+ jv.multi_currency = 1
+
jv.append("accounts", reconcilation_entry)
jv.append("accounts", advance_entry)
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
index 12d52ea51fd..34a3900015d 100644
--- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
@@ -8,7 +8,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.query_builder import DocType, Interval
from frappe.query_builder.functions import Now
-from frappe.utils import cint, cstr
+from frappe.utils import cint, cstr, date_diff, today
from erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils import (
get_leaf_boms,
@@ -88,10 +88,12 @@ class BOMUpdateLog(Document):
wip_log = frappe.get_all(
"BOM Update Log",
- {"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
+ fields=["name", "modified"],
+ filters={"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
limit_page_length=1,
)
- if wip_log:
+
+ if wip_log and date_diff(today(), wip_log[0].modified) < 1:
log_link = frappe.utils.get_link_to_form("BOM Update Log", wip_log[0].name)
frappe.throw(
_("BOM Updation already in progress. Please wait until {0} is complete.").format(log_link),
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 2a97061a6a2..5b1455ef723 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -371,6 +371,8 @@ erpnext.patches.v15_0.update_warehouse_field_in_asset_repair_consumed_item_docty
erpnext.patches.v15_0.update_asset_repair_field_in_stock_entry
erpnext.patches.v15_0.update_total_number_of_booked_depreciations
erpnext.patches.v15_0.do_not_use_batchwise_valuation
+erpnext.patches.v15_0.update_invoice_remarks
+erpnext.patches.v14_0.update_reports_with_range
erpnext.patches.v15_0.drop_index_posting_datetime_from_sle
erpnext.patches.v15_0.add_disassembly_order_stock_entry_type #1
erpnext.patches.v15_0.set_standard_stock_entry_type
diff --git a/erpnext/patches/v14_0/update_reports_with_range.py b/erpnext/patches/v14_0/update_reports_with_range.py
new file mode 100644
index 00000000000..2bda265ca66
--- /dev/null
+++ b/erpnext/patches/v14_0/update_reports_with_range.py
@@ -0,0 +1,36 @@
+import json
+
+import frappe
+
+REFERENCE_REPORTS = [
+ "Accounts Receivable",
+ "Accounts Receivable Summary",
+ "Accounts Payable",
+ "Accounts Payable Summary",
+ "Stock Ageing",
+]
+
+
+def execute():
+ for report in REFERENCE_REPORTS:
+ update_reference_reports(report)
+
+
+def update_reference_reports(reference_report):
+ reports = frappe.get_all(
+ "Report", filters={"reference_report": reference_report}, fields={"json", "name"}
+ )
+
+ for report in reports:
+ update_report_json(report)
+ update_reference_reports(report.name)
+
+
+def update_report_json(report):
+ report_json = json.loads(report.json)
+ report_filter = report_json.get("filters")
+
+ keys_to_pop = [key for key in report_filter if key.startswith("range")]
+ report_filter["range"] = ", ".join(str(report_filter.pop(key)) for key in keys_to_pop)
+
+ frappe.db.set_value("Report", report.name, "json", json.dumps(report_json))
diff --git a/erpnext/patches/v15_0/update_invoice_remarks.py b/erpnext/patches/v15_0/update_invoice_remarks.py
new file mode 100644
index 00000000000..7060fe57e31
--- /dev/null
+++ b/erpnext/patches/v15_0/update_invoice_remarks.py
@@ -0,0 +1,51 @@
+import frappe
+from frappe import _
+
+
+def execute():
+ update_sales_invoice_remarks()
+ update_purchase_invoice_remarks()
+
+
+def update_sales_invoice_remarks():
+ si_list = frappe.db.get_all(
+ "Sales Invoice",
+ filters={
+ "docstatus": 1,
+ "remarks": "No Remarks",
+ "po_no": ["!=", ""],
+ },
+ fields=["name", "po_no"],
+ )
+
+ for doc in si_list:
+ remarks = _("Against Customer Order {0}").format(doc.po_no)
+ update_remarks("Sales Invoice", doc.name, remarks)
+
+
+def update_purchase_invoice_remarks():
+ pi_list = frappe.db.get_all(
+ "Purchase Invoice",
+ filters={
+ "docstatus": 1,
+ "remarks": "No Remarks",
+ "bill_no": ["!=", ""],
+ },
+ fields=["name", "bill_no"],
+ )
+
+ for doc in pi_list:
+ remarks = _("Against Supplier Invoice {0}").format(doc.bill_no)
+ update_remarks("Purchase Invoice", doc.name, remarks)
+
+
+def update_remarks(doctype, docname, remarks):
+ filters = {
+ "voucher_type": doctype,
+ "remarks": "No Remarks",
+ "voucher_no": docname,
+ }
+
+ frappe.db.set_value(doctype, docname, "remarks", remarks)
+ frappe.db.set_value("GL Entry", filters, "remarks", remarks)
+ frappe.db.set_value("Payment Ledger Entry", filters, "remarks", remarks)
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index d8bf4a5cf17..3dcd36b2cff 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1510,8 +1510,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
item_grid.set_column_disp(fname, me.frm.doc.currency != company_currency);
});
- var show = (cint(cur_frm.doc.discount_amount)) ||
- ((cur_frm.doc.taxes || []).filter(function(d) {return d.included_in_print_rate===1}).length);
+ var show = (cint(this.frm.doc.discount_amount)) ||
+ ((this.frm.doc.taxes || []).filter(function(d) {return d.included_in_print_rate===1}).length);
$.each(["net_rate", "net_amount"], function(i, fname) {
if(frappe.meta.get_docfield(item_grid.doctype, fname))
@@ -1777,7 +1777,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (data && data.apply_rule_on_other_items && JSON.parse(data.apply_rule_on_other_items)) {
me.frm.doc.items.forEach(d => {
- if (in_list(JSON.parse(data.apply_rule_on_other_items), d[data.apply_rule_on])) {
+ if (in_list(JSON.parse(data.apply_rule_on_other_items), d[data.apply_rule_on]) && d.item_code === data.item_code) {
for(var k in data) {
if (data.pricing_rule_for == "Discount Percentage" && data.apply_rule_on_other_items && k == "discount_amount") {
continue;
@@ -1879,6 +1879,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
const fields = ["discount_percentage",
"discount_amount", "margin_rate_or_amount", "rate_with_margin"];
+ if (!item) {
+ return;
+ }
+
if(item.remove_free_item) {
let items = [];
diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js
index 6864e2865d3..c6ee8a330c7 100644
--- a/erpnext/public/js/utils/unreconcile.js
+++ b/erpnext/public/js/utils/unreconcile.js
@@ -69,7 +69,7 @@ erpnext.accounts.unreconcile_payment = {
{
label: __("Voucher Type"),
fieldname: "voucher_type",
- fieldtype: "Dynamic Link",
+ fieldtype: "Link",
options: "DocType",
in_list_view: 1,
read_only: 1,
@@ -77,7 +77,7 @@ erpnext.accounts.unreconcile_payment = {
{
label: __("Voucher No"),
fieldname: "voucher_no",
- fieldtype: "Link",
+ fieldtype: "Dynamic Link",
options: "voucher_type",
in_list_view: 1,
read_only: 1,
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 570677aad78..2f2d840cce8 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -185,6 +185,8 @@ class SalesOrder(SellingController):
super().__init__(*args, **kwargs)
def onload(self) -> None:
+ super().onload()
+
if frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
if self.has_unreserved_stock():
self.set_onload("has_unreserved_stock", True)
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 8a096aca80c..5d95e7b66d3 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -208,6 +208,8 @@ class DeliveryNote(SellingController):
)
def onload(self):
+ super().onload()
+
if self.docstatus == 0:
self.set_onload("has_unpacked_items", self.has_unpacked_items())
@@ -360,52 +362,33 @@ class DeliveryNote(SellingController):
self.validate_sales_invoice_references()
def validate_sales_order_references(self):
- err_msg = ""
- for item in self.items:
- if (item.against_sales_order and not item.so_detail) or (
- not item.against_sales_order and item.so_detail
- ):
- if not item.against_sales_order:
- err_msg += (
- _("'Sales Order' reference ({1}) is missing in row {0}").format(
- frappe.bold(item.idx), frappe.bold("against_sales_order")
- )
- + "
"
- )
- else:
- err_msg += (
- _("'Sales Order Item' reference ({1}) is missing in row {0}").format(
- frappe.bold(item.idx), frappe.bold("so_detail")
- )
- + "
"
- )
-
- if err_msg:
- frappe.throw(err_msg, title=_("References to Sales Orders are Incomplete"))
+ self._validate_dependent_item_fields(
+ "against_sales_order", "so_detail", _("References to Sales Orders are Incomplete")
+ )
def validate_sales_invoice_references(self):
- err_msg = ""
- for item in self.items:
- if (item.against_sales_invoice and not item.si_detail) or (
- not item.against_sales_invoice and item.si_detail
- ):
- if not item.against_sales_invoice:
- err_msg += (
- _("'Sales Invoice' reference ({1}) is missing in row {0}").format(
- frappe.bold(item.idx), frappe.bold("against_sales_invoice")
- )
- + "
"
- )
- else:
- err_msg += (
- _("'Sales Invoice Item' reference ({1}) is missing in row {0}").format(
- frappe.bold(item.idx), frappe.bold("si_detail")
- )
- + "
"
- )
+ self._validate_dependent_item_fields(
+ "against_sales_invoice", "si_detail", _("References to Sales Invoices are Incomplete")
+ )
- if err_msg:
- frappe.throw(err_msg, title=_("References to Sales Invoices are Incomplete"))
+ def _validate_dependent_item_fields(self, field_a: str, field_b: str, error_title: str):
+ errors = []
+ for item in self.items:
+ missing_label = None
+ if item.get(field_a) and not item.get(field_b):
+ missing_label = item.meta.get_label(field_b)
+ elif item.get(field_b) and not item.get(field_a):
+ missing_label = item.meta.get_label(field_a)
+
+ if missing_label and missing_label != "No Label":
+ errors.append(
+ _("The field {0} in row {1} is not set").format(
+ frappe.bold(_(missing_label)), frappe.bold(item.idx)
+ )
+ )
+
+ if errors:
+ frappe.throw("
".join(errors), title=error_title)
def validate_proj_cust(self):
"""check for does customer belong to same project as entered.."""
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 0c93fd6c402..27a37ef1bab 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -650,6 +650,8 @@ class PickList(Document):
if self.name:
query = query.where(pi_item.parent != self.name)
+ query = query.for_update()
+
return query.run(as_dict=True)
def _get_product_bundles(self) -> dict[str, str]:
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index ef4d475dca4..956eb08d9ff 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -1558,13 +1558,14 @@ def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos):
serial_nos = set()
data = get_stock_ledgers_for_serial_nos(kwargs)
+ bundle_wise_serial_nos = get_bundle_wise_serial_nos(data)
for d in data:
if d.serial_and_batch_bundle:
- sns = get_serial_nos_from_bundle(d.serial_and_batch_bundle, kwargs.get("serial_nos", []))
- if d.actual_qty > 0:
- serial_nos.update(sns)
- else:
- serial_nos.difference_update(sns)
+ if sns := bundle_wise_serial_nos.get(d.serial_and_batch_bundle):
+ if d.actual_qty > 0:
+ serial_nos.update(sns)
+ else:
+ serial_nos.difference_update(sns)
elif d.serial_no:
sns = get_serial_nos(d.serial_no)
@@ -1581,6 +1582,25 @@ def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos):
return serial_nos
+def get_bundle_wise_serial_nos(data):
+ bundle_wise_serial_nos = defaultdict(list)
+ bundles = [d.serial_and_batch_bundle for d in data if d.serial_and_batch_bundle]
+ if not bundles:
+ return bundle_wise_serial_nos
+
+ bundle_data = frappe.get_all(
+ "Serial and Batch Entry",
+ fields=["serial_no", "parent"],
+ filters={"parent": ("in", bundles), "docstatus": 1, "serial_no": ("is", "set")},
+ )
+
+ for d in bundle_data:
+ if d.parent:
+ bundle_wise_serial_nos[d.parent].append(d.serial_no)
+
+ return bundle_wise_serial_nos
+
+
def get_reserved_serial_nos(kwargs) -> list:
"""Returns a list of `Serial No` reserved in POS Invoice and Stock Reservation Entry."""
diff --git a/erpnext/stock/report/available_batch_report/available_batch_report.js b/erpnext/stock/report/available_batch_report/available_batch_report.js
index 011f7e09ca2..a13e4ca82f7 100644
--- a/erpnext/stock/report/available_batch_report/available_batch_report.js
+++ b/erpnext/stock/report/available_batch_report/available_batch_report.js
@@ -17,7 +17,7 @@ frappe.query_reports["Available Batch Report"] = {
fieldtype: "Date",
width: "80",
reqd: 1,
- default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ default: frappe.datetime.get_today(),
},
{
fieldname: "item_code",
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.js b/erpnext/stock/report/stock_ageing/stock_ageing.js
index 578869b6e93..a3ebf14571d 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.js
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.js
@@ -54,25 +54,10 @@ frappe.query_reports["Stock Ageing"] = {
options: "Brand",
},
{
- fieldname: "range1",
- label: __("Ageing Range 1"),
- fieldtype: "Int",
- default: "30",
- reqd: 1,
- },
- {
- fieldname: "range2",
- label: __("Ageing Range 2"),
- fieldtype: "Int",
- default: "60",
- reqd: 1,
- },
- {
- fieldname: "range3",
- label: __("Ageing Range 3"),
- fieldtype: "Int",
- default: "90",
- reqd: 1,
+ fieldname: "range",
+ label: __("Ageing Range"),
+ fieldtype: "Data",
+ default: "30, 60, 90",
},
{
fieldname: "show_warehouse_wise_stock",
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index 09af3f224a3..c7e0af6cd38 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -16,6 +16,7 @@ Filters = frappe._dict
def execute(filters: Filters = None) -> tuple:
to_date = filters["to_date"]
+ filters.ranges = [num.strip() for num in filters.range.split(",") if num.strip().isdigit()]
columns = get_columns(filters)
item_details = FIFOSlots(filters).generate()
@@ -48,7 +49,7 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
average_age = get_average_age(fifo_queue, to_date)
earliest_age = date_diff(to_date, fifo_queue[0][1])
latest_age = date_diff(to_date, fifo_queue[-1][1])
- range1, range2, range3, above_range3 = get_range_age(filters, fifo_queue, to_date, item_dict)
+ range_values = get_range_age(filters, fifo_queue, to_date, item_dict)
row = [details.name, details.item_name, details.description, details.item_group, details.brand]
@@ -59,10 +60,7 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
[
flt(item_dict.get("total_qty"), precision),
average_age,
- range1,
- range2,
- range3,
- above_range3,
+ *range_values,
earliest_age,
latest_age,
details.stock_uom,
@@ -89,25 +87,22 @@ def get_average_age(fifo_queue: list, to_date: str) -> float:
return flt(age_qty / total_qty, 2) if total_qty else 0.0
-def get_range_age(filters: Filters, fifo_queue: list, to_date: str, item_dict: dict) -> tuple:
+def get_range_age(filters: Filters, fifo_queue: list, to_date: str, item_dict: dict) -> list:
precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
-
- range1 = range2 = range3 = above_range3 = 0.0
+ range_values = [0.0] * (len(filters.ranges) + 1)
for item in fifo_queue:
age = flt(date_diff(to_date, item[1]))
qty = flt(item[0]) if not item_dict["has_serial_no"] else 1.0
- if age <= flt(filters.range1):
- range1 = flt(range1 + qty, precision)
- elif age <= flt(filters.range2):
- range2 = flt(range2 + qty, precision)
- elif age <= flt(filters.range3):
- range3 = flt(range3 + qty, precision)
+ for i, age_limit in enumerate(filters.ranges):
+ if age <= flt(age_limit):
+ range_values[i] = flt(range_values[i] + qty, precision)
+ break
else:
- above_range3 = flt(above_range3 + qty, precision)
+ range_values[-1] = flt(range_values[-1] + qty, precision)
- return range1, range2, range3, above_range3
+ return range_values
def get_columns(filters: Filters) -> list[dict]:
@@ -193,12 +188,14 @@ def get_chart_data(data: list, filters: Filters) -> dict:
def setup_ageing_columns(filters: Filters, range_columns: list):
- ranges = [
- f"0 - {filters['range1']}",
- f"{cint(filters['range1']) + 1} - {cint(filters['range2'])}",
- f"{cint(filters['range2']) + 1} - {cint(filters['range3'])}",
- _("{0} - Above").format(cint(filters["range3"]) + 1),
- ]
+ prev_range_value = 0
+ ranges = []
+ for range in filters.ranges:
+ ranges.append(f"{prev_range_value} - {range}")
+ prev_range_value = cint(range) + 1
+
+ ranges.append(f"{prev_range_value} - Above")
+
for i, label in enumerate(ranges):
fieldname = "range" + str(i + 1)
add_column(range_columns, label=_("Age ({0})").format(label), fieldname=fieldname)
diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
index fb363606233..c0c007e5015 100644
--- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
@@ -9,9 +9,7 @@ from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, format_rep
class TestStockAgeing(FrappeTestCase):
def setUp(self) -> None:
- self.filters = frappe._dict(
- company="_Test Company", to_date="2021-12-10", range1=30, range2=60, range3=90
- )
+ self.filters = frappe._dict(company="_Test Company", to_date="2021-12-10", ranges=["30", "60", "90"])
def test_normal_inward_outward_queue(self):
"Reference: Case 1 in stock_ageing_fifo_logic.md (same wh)"
diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py
index 74c6afa204b..85337a3bf54 100644
--- a/erpnext/stock/report/test_reports.py
+++ b/erpnext/stock/report/test_reports.py
@@ -62,7 +62,7 @@ REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
("Item Prices", {"items": "Enabled Items only"}),
("Delayed Item Report", {"based_on": "Sales Invoice"}),
("Delayed Item Report", {"based_on": "Delivery Note"}),
- ("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
+ ("Stock Ageing", {"range": "30, 60, 90", "_optional": True}),
("Stock Ledger Invariant Check", {"warehouse": "_Test Warehouse - _TC", "item": "_Test Item"}),
("FIFO Queue vs Qty After Transaction Comparison", {"warehouse": "_Test Warehouse - _TC"}),
("FIFO Queue vs Qty After Transaction Comparison", {"item_group": "All Item Groups"}),