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 b1419020f0f..5bbb2a14b92 100644
--- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
@@ -76,6 +76,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/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 7b471ed418e..9b303430455 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -3113,6 +3113,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
@@ -3634,6 +3678,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 check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql(
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 8240b79bfdc..a19eedd8b72 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 61a3a96d5fe..171a94e1151 100644
--- a/erpnext/accounts/report/accounts_payable/accounts_payable.js
+++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js
@@ -162,6 +162,11 @@ frappe.query_reports["Accounts Payable"] = {
label: __("Group by Voucher"),
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_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 11177a10772..b8341bcfa01 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -112,6 +112,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 +143,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 +212,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"):
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/utils.py b/erpnext/accounts/utils.py
index 0ae4db3673c..b2c4d92a6af 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -739,6 +739,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/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 b8218ce8ddc..a8a790719bf 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1463,6 +1463,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,
)
@@ -1474,6 +1475,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)
@@ -2296,12 +2298,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()
@@ -2335,6 +2340,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 02c43a8beff..388094a9d87 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,
@@ -67,10 +67,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/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/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 18adf6ab1f9..b725eff05b7 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -211,52 +211,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/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",