diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 965f90a1266..cbf8745390e 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -187,7 +187,6 @@ class GLEntry(Document): account_type == "Profit and Loss" and self.company == dimension.company and dimension.mandatory_for_pl - and not dimension.disabled and not self.is_cancelled ): if not self.get(dimension.fieldname): @@ -201,7 +200,6 @@ class GLEntry(Document): account_type == "Balance Sheet" and self.company == dimension.company and dimension.mandatory_for_bs - and not dimension.disabled and not self.is_cancelled ): if not self.get(dimension.fieldname): diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 0aa48dcd721..e7c0832554f 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -177,6 +177,9 @@ class JournalEntry(AccountsController): else: return self._submit() + def before_cancel(self): + self.has_asset_adjustment_entry() + def cancel(self): if len(self.accounts) > 100: queue_submission(self, "_cancel") @@ -447,12 +450,27 @@ class JournalEntry(AccountsController): ) frappe.db.set_value("Journal Entry", self.name, "inter_company_journal_entry_reference", "") - def unlink_asset_adjustment_entry(self): - frappe.db.sql( - """ update `tabAsset Value Adjustment` - set journal_entry = null where journal_entry = %s""", - self.name, + def has_asset_adjustment_entry(self): + if self.flags.get("via_asset_value_adjustment"): + return + + asset_value_adjustment = frappe.db.get_value( + "Asset Value Adjustment", {"docstatus": 1, "journal_entry": self.name}, "name" ) + if asset_value_adjustment: + frappe.throw( + _( + "Cannot cancel this document as it is linked with the submitted Asset Value Adjustment {0}. Please cancel the Asset Value Adjustment to continue." + ).format(frappe.utils.get_link_to_form("Asset Value Adjustment", asset_value_adjustment)) + ) + + def unlink_asset_adjustment_entry(self): + AssetValueAdjustment = frappe.qb.DocType("Asset Value Adjustment") + ( + frappe.qb.update(AssetValueAdjustment) + .set(AssetValueAdjustment.journal_entry, None) + .where(AssetValueAdjustment.journal_entry == self.name) + ).run() def validate_party(self): for d in self.get("accounts"): diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index 111de2ebd4e..e8411fffb02 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -68,7 +68,7 @@ { "columns": 2, "fieldname": "total_amount", - "fieldtype": "Float", + "fieldtype": "Currency", "in_list_view": 1, "label": "Grand Total", "print_hide": 1, @@ -77,7 +77,7 @@ { "columns": 2, "fieldname": "outstanding_amount", - "fieldtype": "Float", + "fieldtype": "Currency", "in_list_view": 1, "label": "Outstanding", "read_only": 1 @@ -85,7 +85,7 @@ { "columns": 2, "fieldname": "allocated_amount", - "fieldtype": "Float", + "fieldtype": "Currency", "in_list_view": 1, "label": "Allocated" }, @@ -174,7 +174,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-07-25 04:32:11.040025", + "modified": "2026-01-05 14:18:03.286224", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py index a5e0b21a9af..0091d792f33 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py @@ -18,12 +18,12 @@ class PaymentEntryReference(Document): account_type: DF.Data | None advance_voucher_no: DF.DynamicLink | None advance_voucher_type: DF.Link | None - allocated_amount: DF.Float + allocated_amount: DF.Currency bill_no: DF.Data | None due_date: DF.Date | None exchange_gain_loss: DF.Currency exchange_rate: DF.Float - outstanding_amount: DF.Float + outstanding_amount: DF.Currency parent: DF.Data parentfield: DF.Data parenttype: DF.Data @@ -34,7 +34,7 @@ class PaymentEntryReference(Document): reconcile_effect_on: DF.Date | None reference_doctype: DF.Link reference_name: DF.DynamicLink - total_amount: DF.Float + total_amount: DF.Currency # end: auto-generated types @property diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py index 2bc44893c20..c7cc97d7197 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py @@ -133,7 +133,6 @@ class PaymentLedgerEntry(Document): account_type == "Profit and Loss" and self.company == dimension.company and dimension.mandatory_for_pl - and not dimension.disabled ): if not self.get(dimension.fieldname): frappe.throw( @@ -146,7 +145,6 @@ class PaymentLedgerEntry(Document): account_type == "Balance Sheet" and self.company == dimension.company and dimension.mandatory_for_bs - and not dimension.disabled ): if not self.get(dimension.fieldname): frappe.throw( diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index b73100c78f2..c81863f044f 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -6,7 +6,7 @@ import frappe from frappe import _, msgprint, qb from frappe.model.document import Document from frappe.model.meta import get_field_precision -from frappe.query_builder import Criterion +from frappe.query_builder import Case, Criterion from frappe.query_builder.custom import ConstantColumn from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today @@ -393,6 +393,9 @@ class PaymentReconciliation(Document): inv.outstanding_amount = flt(entry.get("outstanding_amount")) def get_difference_amount(self, payment_entry, invoice, allocated_amount): + party_account_defaults = frappe.get_cached_value( + "Account", self.receivable_payable_account, ["account_type", "account_currency"], as_dict=True + ) allocated_amount_precision = get_field_precision( frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount") ) @@ -400,9 +403,9 @@ class PaymentReconciliation(Document): frappe.get_meta("Payment Reconciliation Allocation").get_field("difference_amount") ) difference_amount = 0 - if frappe.get_cached_value( - "Account", self.receivable_payable_account, "account_currency" - ) != frappe.get_cached_value("Company", self.company, "default_currency"): + if party_account_defaults.get("account_currency") != frappe.get_cached_value( + "Company", self.company, "default_currency" + ): if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get( "exchange_rate", 1 ): @@ -414,7 +417,14 @@ class PaymentReconciliation(Document): invoice.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision), difference_amount_precision, ) - difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate + + # Added If clause to handle return Adhoc payments for account type holders ("Payable") + if party_account_defaults.get("account_type") in ("Payable") and invoice.get( + "invoice_type" + ) in ["Payment Entry", "Journal Entry"]: + difference_amount = allocated_amount_in_inv_rate - allocated_amount_in_ref_rate + else: + difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate return difference_amount @@ -677,6 +687,28 @@ class PaymentReconciliation(Document): ) invoice_exchange_map.update(journals_map) + payment_entries = [ + d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Payment Entry" + ] + payment_entries.extend( + [d.get("reference_name") for d in payments if d.get("reference_type") == "Payment Entry"] + ) + if payment_entries: + pe = frappe.qb.DocType("Payment Entry") + query = ( + frappe.qb.from_(pe) + .select( + pe.name, + Case() + .when(pe.payment_type == "Receive", pe.source_exchange_rate) + .else_(pe.target_exchange_rate) + .as_("exchange_rate"), + ) + .where(pe.name.isin(payment_entries)) + ) + payment_entries = query.run(as_list=1) + invoice_exchange_map.update(payment_entries) + return invoice_exchange_map def validate_allocation(self): diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 679eb90f19a..59c385855fa 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -2336,6 +2336,210 @@ class TestPaymentReconciliation(FrappeTestCase): frappe.db.set_value("Company", self.company, default_settings) + def test_foreign_currency_reverse_payment_entry_against_payment_entry_for_customer(self): + transaction_date = nowdate() + customer = self.customer3 + amount = 1000 + exchange_rate_at_payment = 100 + exchange_rate_at_reverse_payment = 95 + + # Receive amount from customer - 1,00,000 + pe = self.create_payment_entry(amount=amount, posting_date=transaction_date, customer=customer) + pe.payment_type = "Receive" + pe.paid_from = self.debtors_eur + pe.paid_from_account_currency = "EUR" + pe.source_exchange_rate = exchange_rate_at_payment + pe.paid_amount = amount + pe.received_amount = exchange_rate_at_payment * amount + pe.paid_to = self.cash + pe.paid_to_account_currency = "INR" + pe = pe.save().submit() + + # Pay amount to customer - 95,000 + reverse_pe = self.create_payment_entry( + amount=amount, posting_date=transaction_date, customer=customer + ) + reverse_pe.payment_type = "Pay" + reverse_pe.paid_from = self.cash + reverse_pe.paid_from_account_currency = "INR" + reverse_pe.target_exchange_rate = exchange_rate_at_reverse_payment + reverse_pe.paid_amount = exchange_rate_at_reverse_payment * amount + reverse_pe.received_amount = amount + reverse_pe.paid_to = self.debtors_eur + reverse_pe.paid_to_account_currency = "EUR" + reverse_pe.save().submit() + + # Reconcile payments + pr = self.create_payment_reconciliation() + pr.party = customer + pr.receivable_payable_account = self.debtors_eur + pr.get_unreconciled_entries() + invoices = [invoice.as_dict() for invoice in pr.invoices] + payments = [payment.as_dict() for payment in pr.payments] + self.assertEqual(len(pr.get("invoices")), 1) + self.assertEqual(len(pr.get("payments")), 1) + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Check the difference_amount is a gain of 5000 + self.assertEqual(flt(pr.allocation[0].get("difference_amount")), 5000.0) + pr.reconcile() + + def test_foreign_currency_reverse_payment_entry_against_payment_entry_for_supplier(self): + transaction_date = nowdate() + self.supplier = "_Test Supplier USD" + amount = 1000 + exchange_rate_at_payment = 100 + exchange_rate_at_reverse_payment = 95 + + # Pay amount to supplier - 1,00,000 + pe = self.create_payment_entry(amount=amount, posting_date=transaction_date) + pe.payment_type = "Pay" + pe.party_type = "Supplier" + pe.party = self.supplier + pe.paid_from = self.cash + pe.paid_from_account_currency = "INR" + pe.target_exchange_rate = exchange_rate_at_payment + pe.paid_amount = exchange_rate_at_payment * amount + pe.received_amount = amount + pe.paid_to = self.creditors_usd + pe.paid_to_account_currency = "USD" + pe.save().submit() + + # Receive amount from supplier - 95,000 + reverse_pe = self.create_payment_entry(amount=amount, posting_date=transaction_date) + reverse_pe.payment_type = "Receive" + reverse_pe.party_type = "Supplier" + reverse_pe.party = self.supplier + reverse_pe.paid_from = self.creditors_usd + reverse_pe.paid_from_account_currency = "USD" + reverse_pe.source_exchange_rate = exchange_rate_at_reverse_payment + reverse_pe.paid_amount = amount + reverse_pe.received_amount = exchange_rate_at_reverse_payment * amount + reverse_pe.paid_to = self.cash + reverse_pe.paid_to_account_currency = "INR" + reverse_pe = reverse_pe.save().submit() + + # Reconcile payments + pr = self.create_payment_reconciliation(party_is_customer=False) + pr.party = self.supplier + pr.receivable_payable_account = self.creditors_usd + pr.get_unreconciled_entries() + invoices = [invoice.as_dict() for invoice in pr.invoices] + payments = [payment.as_dict() for payment in pr.payments] + + self.assertEqual(len(pr.get("invoices")), 1) + self.assertEqual(len(pr.get("payments")), 1) + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Check the difference_amount is a loss of 5000 + self.assertEqual(flt(pr.allocation[0].get("difference_amount")), -5000.0) + pr.reconcile() + + def test_foreign_currency_reverse_journal_entry_against_journal_entry_for_customer(self): + transaction_date = nowdate() + customer = self.customer3 + amount = 1000 + exchange_rate_at_payment = 95 + exchange_rate_at_reverse_payment = 100 + + # Receive amount from customer - 95,000 + je1 = self.create_journal_entry(self.cash, self.debtors_eur, amount, transaction_date) + je1.multi_currency = 1 + je1.accounts[0].exchange_rate = 1 + je1.accounts[0].debit_in_account_currency = exchange_rate_at_payment * amount + je1.accounts[0].debit = exchange_rate_at_payment * amount + je1.accounts[1].party_type = "Customer" + je1.accounts[1].party = customer + je1.accounts[1].exchange_rate = exchange_rate_at_payment + je1.accounts[1].credit_in_account_currency = amount + je1.accounts[1].credit = exchange_rate_at_payment * amount + je1.save() + je1.submit() + + # Pay amount to customer - 1,00,000 + je2 = self.create_journal_entry(self.debtors_eur, self.cash, amount, transaction_date) + je2.multi_currency = 1 + je2.accounts[0].party_type = "Customer" + je2.accounts[0].party = customer + je2.accounts[0].exchange_rate = exchange_rate_at_reverse_payment + je2.accounts[0].debit_in_account_currency = amount + je2.accounts[0].debit = exchange_rate_at_reverse_payment * amount + je2.accounts[1].exchange_rate = 1 + je2.accounts[1].credit_in_account_currency = exchange_rate_at_reverse_payment * amount + je2.accounts[1].credit = exchange_rate_at_reverse_payment * amount + je2.save() + je2.submit() + + # Reconcile payments + pr = self.create_payment_reconciliation() + pr.party = customer + pr.receivable_payable_account = self.debtors_eur + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + + invoices = [invoice.as_dict() for invoice in pr.invoices] + payments = [payment.as_dict() for payment in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Check the difference_amount is a loss of 5000 + self.assertEqual(flt(pr.allocation[0].difference_amount), -5000.0) + pr.reconcile() + + def test_foreign_currency_reverse_journal_entry_against_journal_entry_for_supplier(self): + transaction_date = nowdate() + self.supplier = "_Test Supplier USD" + amount = 1000 + exchange_rate_at_payment = 95 + exchange_rate_at_reverse_payment = 100 + + # Pay amount to supplier - 95,000 + je1 = self.create_journal_entry(self.creditors_usd, self.cash, amount, transaction_date) + je1.multi_currency = 1 + je1.accounts[0].party_type = "Supplier" + je1.accounts[0].party = self.supplier + je1.accounts[0].exchange_rate = exchange_rate_at_payment + je1.accounts[0].debit_in_account_currency = amount + je1.accounts[0].debit = exchange_rate_at_payment * amount + je1.accounts[1].exchange_rate = 1 + je1.accounts[1].credit = exchange_rate_at_payment * amount + je1.accounts[1].credit_in_account_currency = exchange_rate_at_payment * amount + je1.save() + je1.submit() + + # Receive amount from supplier - 1,00,000 + je2 = self.create_journal_entry(self.cash, self.creditors_usd, amount, transaction_date) + je2.multi_currency = 1 + je2.accounts[0].exchange_rate = 1 + je2.accounts[0].debit = exchange_rate_at_reverse_payment * amount + je2.accounts[0].debit_in_account_currency = exchange_rate_at_reverse_payment * amount + je2.accounts[1].party_type = "Supplier" + je2.accounts[1].party = self.supplier + je2.accounts[1].exchange_rate = exchange_rate_at_reverse_payment + je2.accounts[1].credit_in_account_currency = amount + je2.accounts[1].credit = exchange_rate_at_reverse_payment * amount + je2.save() + je2.submit() + + # Reconcile payments + pr = self.create_payment_reconciliation() + pr.party_type = "Supplier" + pr.party = self.supplier + pr.receivable_payable_account = self.creditors_usd + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + + invoices = [invoice.as_dict() for invoice in pr.invoices] + payments = [payment.as_dict() for payment in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Check the difference_amount is a gain of 5000 + self.assertEqual(flt(pr.allocation[0].difference_amount), 5000.0) + pr.reconcile() + def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index b0c16ac27d1..97b3e87770f 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -481,6 +481,7 @@ class TestPOSInvoice(unittest.TestCase): rate=1000, serial_no=[serial_nos[0]], do_not_save=1, + ignore_sabb_validation=True, ) pos2.append("payments", {"mode_of_payment": "Bank Draft", "amount": 1000}) @@ -956,6 +957,7 @@ class TestPOSInvoice(unittest.TestCase): qty=1, rate=100, do_not_submit=True, + ignore_sabb_validation=True, ) self.assertRaises(frappe.ValidationError, pos_inv.submit) @@ -1097,6 +1099,7 @@ def create_pos_invoice(**args): "posting_time": pos_inv.posting_time, "type_of_transaction": type_of_transaction, "do_not_submit": True, + "ignore_sabb_validation": args.ignore_sabb_validation, } ) ).name diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 13201f4bcbc..60b3efd3cbc 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -12,6 +12,7 @@ from frappe.utils import cint, flt, formatdate, get_link_to_form, getdate, now import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, + get_checks_for_pl_and_bs_accounts, ) from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import ( get_dimension_filter_map, @@ -612,6 +613,18 @@ def update_accounting_dimensions(round_off_gle): for dimension in dimensions: round_off_gle[dimension] = dimension_values.get(dimension) + else: + report_type = frappe.get_cached_value("Account", round_off_gle.account, "report_type") + for dimension in get_checks_for_pl_and_bs_accounts(): + if ( + round_off_gle.company == dimension.company + and ( + (report_type == "Profit and Loss" and dimension.mandatory_for_pl) + or (report_type == "Balance Sheet" and dimension.mandatory_for_bs) + ) + and dimension.default_dimension + ): + round_off_gle[dimension.fieldname] = dimension.default_dimension def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use_company_default=False): diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index e2ba7814a3d..f81b26b2c8d 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -235,26 +235,64 @@ frappe.ui.form.on("Asset", { }, toggle_reference_doc: function (frm) { - if (frm.doc.purchase_receipt && frm.doc.purchase_invoice && frm.doc.docstatus === 1) { - frm.set_df_property("purchase_invoice", "read_only", 1); - frm.set_df_property("purchase_receipt", "read_only", 1); - } else if (frm.doc.is_existing_asset || frm.doc.is_composite_asset) { - frm.toggle_reqd("purchase_receipt", 0); - frm.toggle_reqd("purchase_invoice", 0); - } else if (frm.doc.purchase_receipt) { - // if purchase receipt link is set then set PI disabled - frm.toggle_reqd("purchase_invoice", 0); - frm.set_df_property("purchase_invoice", "read_only", 1); - } else if (frm.doc.purchase_invoice) { - // if purchase invoice link is set then set PR disabled - frm.toggle_reqd("purchase_receipt", 0); - frm.set_df_property("purchase_receipt", "read_only", 1); - } else { - frm.toggle_reqd("purchase_receipt", 1); - frm.set_df_property("purchase_receipt", "read_only", 0); - frm.toggle_reqd("purchase_invoice", 1); - frm.set_df_property("purchase_invoice", "read_only", 0); + const is_submitted = frm.doc.docstatus === 1; + const is_special_asset = frm.doc.is_existing_asset || frm.doc.is_composite_asset; + + const clear_field = (field) => { + if (frm.doc[field]) { + frm.set_value(field, ""); + } + }; + + ["purchase_receipt", "purchase_receipt_item", "purchase_invoice", "purchase_invoice_item"].forEach( + (field) => { + frm.toggle_reqd(field, 0); + frm.set_df_property(field, "read_only", 0); + } + ); + + if (is_submitted) { + [ + "purchase_receipt", + "purchase_receipt_item", + "purchase_invoice", + "purchase_invoice_item", + ].forEach((field) => { + frm.set_df_property(field, "read_only", 1); + }); + return; } + + if (is_special_asset) { + clear_field("purchase_receipt"); + clear_field("purchase_receipt_item"); + clear_field("purchase_invoice"); + clear_field("purchase_invoice_item"); + return; + } + + if (frm.doc.purchase_receipt) { + frm.toggle_reqd("purchase_receipt_item", 1); + + ["purchase_invoice", "purchase_invoice_item"].forEach((field) => { + clear_field(field); + frm.set_df_property(field, "read_only", 1); + }); + return; + } + + if (frm.doc.purchase_invoice) { + frm.toggle_reqd("purchase_invoice_item", 1); + + ["purchase_receipt", "purchase_receipt_item"].forEach((field) => { + clear_field(field); + frm.set_df_property(field, "read_only", 1); + }); + return; + } + + frm.toggle_reqd("purchase_receipt", 1); + frm.toggle_reqd("purchase_invoice", 1); }, make_journal_entry: function (frm) { @@ -484,7 +522,6 @@ frappe.ui.form.on("Asset", { } else { frm.set_df_property("gross_purchase_amount", "read_only", 0); } - frm.trigger("toggle_reference_doc"); }, diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json index c2de548b2de..fb7a556f2b7 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json @@ -177,6 +177,7 @@ "default": "1", "fieldname": "target_qty", "fieldtype": "Float", + "hidden": 1, "label": "Target Qty" }, { @@ -290,10 +291,10 @@ "options": "Cost Center" }, { - "fieldname": "project", - "fieldtype": "Link", - "label": "Project", - "options": "Project" + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" }, { "fieldname": "dimension_col_break", @@ -324,7 +325,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-01-08 13:14:33.008458", + "modified": "2026-01-13 17:25:01.352568", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization", @@ -362,10 +363,11 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [], "title_field": "title", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 1f671333cfa..674cb3ffa3d 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -76,6 +76,7 @@ class AssetCapitalization(StockController): naming_series: DF.Literal["ACC-ASC-.YYYY.-"] posting_date: DF.Date posting_time: DF.Time + project: DF.Link | None service_items: DF.Table[AssetCapitalizationServiceItem] service_items_total: DF.Currency set_posting_time: DF.Check diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 31fd62095df..756b95f8fbd 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -57,7 +57,7 @@ class AssetValueAdjustment(Document): ) def on_cancel(self): - frappe.get_doc("Journal Entry", self.journal_entry).cancel() + self.cancel_asset_revaluation_entry() self.update_asset() add_asset_activity( self.asset, @@ -159,6 +159,16 @@ class AssetValueAdjustment(Document): self.db_set("journal_entry", je.name) + def cancel_asset_revaluation_entry(self): + if not self.journal_entry: + return + + revaluation_entry = frappe.get_doc("Journal Entry", self.journal_entry) + if revaluation_entry.docstatus == 1: + revaluation_entry.flags.ignore_permissions = True + revaluation_entry.flags.via_asset_value_adjustment = True + revaluation_entry.cancel() + def update_asset(self, asset_value=None): difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount asset = self.update_asset_value_after_depreciation(difference_amount) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 5cbba410f86..751254babf1 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -794,7 +794,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None): @frappe.whitelist() def make_purchase_invoice_from_portal(purchase_order_name): doc = get_mapped_purchase_invoice(purchase_order_name, ignore_permissions=True) - if doc.contact_email != frappe.session.user: + if frappe.session.user not in frappe.get_all("Portal User", {"parent": doc.supplier}, pluck="user"): frappe.throw(_("Not Permitted"), frappe.PermissionError) doc.save() frappe.db.commit() diff --git a/erpnext/change_log/v15/v15_94_2.md b/erpnext/change_log/v15/v15_94_2.md new file mode 100644 index 00000000000..865ecacb86f --- /dev/null +++ b/erpnext/change_log/v15/v15_94_2.md @@ -0,0 +1,9 @@ +# Version 16 Released! + +ERPNext version 16 has been released! + +Since it's the latest version of ERPNext, we recommend that you update to it to get the latest features, bug fixes and other improvements. + +[Click here to know more about v16](https://frappe.io/erpnext/version-16) + +If you're on [Frappe Cloud](https://frappe.io/cloud), [click here to learn how to update to v16](https://docs.frappe.io/cloud/sites/version-upgrade) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1f6b6ca17c9..172c63bebf6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3714,9 +3714,9 @@ def validate_child_on_delete(row, parent): ) if flt(row.ordered_qty): frappe.throw( - _("Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.").format( - row.idx, row.item_code - ) + _( + "Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order." + ).format(row.idx, row.item_code) ) if parent.doctype == "Purchase Order" and flt(row.received_qty): diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index d74ea55450a..3667a7a7e76 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -1004,10 +1004,19 @@ class SellingController(StockController): def set_default_income_account_for_item(obj): - for d in obj.get("items"): - if d.item_code: - if getattr(d, "income_account", None): - set_item_default(d.item_code, obj.company, "income_account", d.income_account) + """Set income account as default for items in the transaction. + + Updates the item default income account for each item in the transaction + if it differs from the company's default income account. + + Args: + obj: Transaction document containing items table with income_account field + """ + company_default = frappe.get_cached_value("Company", obj.company, "default_income_account") + for d in obj.get("items", default=[]): + income_account = getattr(d, "income_account", None) + if d.item_code and income_account and income_account != company_default: + set_item_default(d.item_code, obj.company, "income_account", income_account) def get_serial_and_batch_bundle(child, parent, delivery_note_child=None): diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index dc80a23198a..106b1cd0c42 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -778,9 +778,8 @@ class TestSubcontractingController(FrappeTestCase): row.serial_no = "ABC" break - bundle.save() + self.assertRaises(frappe.ValidationError, bundle.save) - self.assertRaises(frappe.ValidationError, scr1.save) bundle.load_from_db() for row in bundle.entries: if row.idx == 1: diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 20f8a4ab553..b904d2f13f4 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -316,7 +316,7 @@ class WorkOrder(Document): # already ordered qty ordered_qty_against_so = frappe.db.sql( """select sum(qty) from `tabWork Order` - where production_item = %s and sales_order = %s and docstatus < 2 and name != %s""", + where production_item = %s and sales_order = %s and docstatus < 2 and status != 'Closed' and name != %s""", (self.production_item, self.sales_order, self.name), )[0][0] @@ -351,15 +351,16 @@ class WorkOrder(Document): def update_status(self, status=None): """Update status of work order if unknown""" - if status != "Stopped" and status != "Closed": - status = self.get_status(status) + if self.status != "Closed": + if status not in ["Stopped", "Closed"]: + status = self.get_status(status) - if status != self.status: - self.db_set("status", status) + if status != self.status: + self.db_set("status", status) - self.update_required_items() + self.update_required_items() - return status + return status or self.status def get_status(self, status=None): """Return the status based on stock entries against this work order""" @@ -515,6 +516,9 @@ class WorkOrder(Document): self.validate_cancel() self.db_set("status", "Cancelled") + self.on_close_or_cancel() + + def on_close_or_cancel(self): if self.production_plan and frappe.db.exists( "Production Plan Item Reference", {"parent": self.production_plan} ): @@ -842,7 +846,7 @@ class WorkOrder(Document): qty = frappe.db.sql( f""" select sum(qty) from - `tabWork Order` where sales_order = %s and docstatus = 1 and {cond} + `tabWork Order` where sales_order = %s and docstatus = 1 and status <> 'Closed' and {cond} """, (self.sales_order, (self.product_bundle_item or self.production_item)), as_list=1, @@ -1603,8 +1607,8 @@ def close_work_order(work_order, status): ) ) + work_order.on_close_or_cancel() work_order.update_status(status) - work_order.update_planned_qty() frappe.msgprint(_("Work Order has been {0}").format(status)) work_order.notify_update() return work_order.status diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 1b23042769e..7abdbe75fd5 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -555,10 +555,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe var item = frappe.get_doc(cdt, cdn); var update_stock = 0, show_batch_dialog = 0; - item.weight_per_unit = 0; item.weight_uom = ''; - item.uom = null // make UOM blank to update the existing UOM when item changes + if(!item.barcode){ + item.uom = null // make UOM blank to update the existing UOM when item changes + } item.conversion_factor = 0; if(['Sales Invoice', 'Purchase Invoice'].includes(this.frm.doc.doctype)) { @@ -574,6 +575,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe show_batch_dialog = 0; } + item.barcode = null; diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index 822a9902a38..9f83f04b53f 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -404,6 +404,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { async set_barcode(row, barcode) { if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) { await frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode); + } else { + row.barcode = barcode; } } diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 8be426fa1a5..0079db289a5 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1875,6 +1875,7 @@ def get_work_order_items(sales_order, for_raw_material_request=0): & (wo.sales_order == so.name) & (wo.sales_order_item == i.name) & (wo.docstatus.lt(2)) + & (wo.status != "Closed") ) .run()[0][0] ) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 5e28362c509..29eabe2670e 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -2790,6 +2790,23 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(sre_details[0].reserved_qty, so.items[0].qty) self.assertEqual(sre_details[0].delivered_qty, dn.items[0].qty) + def test_negative_stock_with_higher_precision(self): + original_flt_precision = frappe.db.get_default("float_precision") + frappe.db.set_single_value("System Settings", "float_precision", 7) + + item_code = make_item( + "Test Negative Stock High Precision Item", properties={"is_stock_item": 1, "valuation_rate": 1} + ).name + dn = create_delivery_note( + item_code=item_code, + qty=0.0000010, + do_not_submit=True, + ) + + self.assertRaises(frappe.ValidationError, dn.submit) + + frappe.db.set_single_value("System Settings", "float_precision", original_flt_precision) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index e9c7a813698..7d160b24e2d 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -273,6 +273,9 @@ class MaterialRequest(BuyingController): .groupby(doctype.material_request_item) ) + if self.material_request_type == "Manufacture": + query = query.where(doctype.status != "Closed") + mr_items_ordered_qty = frappe._dict(query.run()) return mr_items_ordered_qty diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index a4a335d2c5c..f82c53bd14f 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -383,7 +383,7 @@ class PickList(TransactionBase): picked_items = get_picked_items_qty(packed_items, contains_packed_items=True) self.validate_picked_qty(picked_items) - doc_updates = {} + doc_updates = {item: {"picked_qty": 0} for item in set(packed_items)} for d in picked_items: doc_updates[d.product_bundle_item] = {"picked_qty": flt(d.picked_qty)} @@ -394,7 +394,7 @@ class PickList(TransactionBase): picked_items = get_picked_items_qty(so_items) self.validate_picked_qty(picked_items) - doc_updates = {} + doc_updates = {item: {"picked_qty": 0} for item in set(so_items)} for d in picked_items: doc_updates[d.sales_order_item] = {"picked_qty": flt(d.picked_qty)} 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 ffd0a0a137d..ad88b571a91 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 @@ -116,10 +116,20 @@ class SerialandBatchBundle(Document): return self.allow_existing_serial_nos() - if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test: - self.validate_serial_nos_duplicate() + if self.docstatus == 1: + if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test: + self.validate_serial_nos_duplicate() + + self.check_future_entries_exists() + + elif ( + self.has_serial_no + and self.type_of_transaction == "Outward" + and self.voucher_type != "Stock Reconciliation" + and self.voucher_no + ): + self.validate_serial_no_status() - self.check_future_entries_exists() self.set_is_outward() self.calculate_total_qty() self.set_warehouse() @@ -129,6 +139,25 @@ class SerialandBatchBundle(Document): self.calculate_qty_and_amount() + def validate_serial_no_status(self): + serial_nos = [d.serial_no for d in self.entries if d.serial_no] + invalid_serial_nos = frappe.get_all( + "Serial No", + filters={ + "name": ("in", serial_nos), + "warehouse": ("!=", self.warehouse), + }, + pluck="name", + ) + + if invalid_serial_nos: + msg = _( + "You cannot outward following {0} as either they are Delivered, Inactive or located in a different warehouse." + ).format(_("Serial Nos") if len(invalid_serial_nos) > 1 else _("Serial No")) + msg += "