diff --git a/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py
index 36ef1fca074..14d4eb3a6ed 100644
--- a/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py
+++ b/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py
@@ -7,6 +7,9 @@ from frappe.utils import nowdate
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
+IBAN_1 = "DE02000000003716541159"
+IBAN_2 = "DE02500105170137075030"
+
class TestAutoMatchParty(FrappeTestCase):
@classmethod
@@ -22,24 +25,24 @@ class TestAutoMatchParty(FrappeTestCase):
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0)
def test_match_by_account_number(self):
- create_supplier_for_match(account_no="000000003716541159")
+ create_supplier_for_match(account_no=IBAN_1[11:])
doc = create_bank_transaction(
withdrawal=1200,
transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b",
- account_no="000000003716541159",
- iban="DE02000000003716541159",
+ account_no=IBAN_1[11:],
+ iban=IBAN_1,
)
self.assertEqual(doc.party_type, "Supplier")
self.assertEqual(doc.party, "John Doe & Co.")
def test_match_by_iban(self):
- create_supplier_for_match(iban="DE02000000003716541159")
+ create_supplier_for_match(iban=IBAN_1)
doc = create_bank_transaction(
withdrawal=1200,
transaction_id="c5455a224602afaa51592a9d9250600d",
- account_no="000000003716541159",
- iban="DE02000000003716541159",
+ account_no=IBAN_1[11:],
+ iban=IBAN_1,
)
self.assertEqual(doc.party_type, "Supplier")
@@ -51,7 +54,7 @@ class TestAutoMatchParty(FrappeTestCase):
withdrawal=1200,
transaction_id="1f6f661f347ff7b1ea588665f473adb1",
party_name="Ella Jackson",
- iban="DE04000000003716545346",
+ iban=IBAN_2,
)
self.assertEqual(doc.party_type, "Supplier")
self.assertEqual(doc.party, "Jackson Ella W.")
diff --git a/erpnext/accounts/doctype/party_link/party_link.py b/erpnext/accounts/doctype/party_link/party_link.py
index 16484fc4bb9..1ed837eada7 100644
--- a/erpnext/accounts/doctype/party_link/party_link.py
+++ b/erpnext/accounts/doctype/party_link/party_link.py
@@ -74,6 +74,6 @@ def create_party_link(primary_role, primary_party, secondary_party):
party_link.secondary_role = "Customer" if primary_role == "Supplier" else "Supplier"
party_link.secondary_party = secondary_party
- party_link.save(ignore_permissions=True)
+ party_link.save()
return party_link
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 816a6bfeded..8cc9598d915 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -281,6 +281,7 @@
"read_only": 1
},
{
+ "fetch_from": "customer.tax_id",
"fieldname": "tax_id",
"fieldtype": "Data",
"hide_days": 1,
@@ -2198,7 +2199,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2025-08-04 19:20:28.732039",
+ "modified": "2025-09-09 14:48:59.472826",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
index 1e3c1ffc598..26d3f93856f 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -369,6 +369,10 @@ class AssetDepreciationSchedule(Document):
original_schedule_date=schedule_date,
)
depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
+
+ if depreciation_amount > row.value_after_depreciation - row.expected_value_after_useful_life:
+ depreciation_amount = row.value_after_depreciation - row.expected_value_after_useful_life
+
if depreciation_amount > 0:
self.add_depr_schedule_row(date_of_disposal, depreciation_amount, n)
@@ -654,6 +658,7 @@ def _get_pro_rata_amt(
total_days = get_total_days(original_schedule_date or to_date, 12)
else:
total_days = get_total_days(original_schedule_date or to_date, row.frequency_of_depreciation)
+
return (depreciation_amount * flt(days)) / flt(total_days), days, months
diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py
index db4e7510670..611f2c44cfd 100644
--- a/erpnext/assets/doctype/asset_movement/asset_movement.py
+++ b/erpnext/assets/doctype/asset_movement/asset_movement.py
@@ -61,7 +61,7 @@ class AssetMovement(Document):
if d.source_location:
if current_location != d.source_location:
frappe.throw(
- _("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location)
+ _("Asset {0} does not belong to the location {1}").format(d.asset, d.source_location)
)
else:
d.source_location = current_location
@@ -75,11 +75,11 @@ class AssetMovement(Document):
frappe.throw(_("Target Location is required while receiving Asset {0}").format(d.asset))
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
frappe.throw(
- _("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
+ _("Employee {0} does not belong to the company {1}").format(d.to_employee, self.company)
)
def validate_employee(self, d):
- if self.purpose == "Tranfer and Issue":
+ if self.purpose == "Transfer and Issue":
if not d.from_employee:
frappe.throw(_("From Employee is required while issuing Asset {0}").format(d.asset))
@@ -88,7 +88,7 @@ class AssetMovement(Document):
if current_custodian != d.from_employee:
frappe.throw(
- _("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee)
+ _("Asset {0} does not belong to the custodian {1}").format(d.asset, d.from_employee)
)
if not d.to_employee:
@@ -96,7 +96,7 @@ class AssetMovement(Document):
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
frappe.throw(
- _("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
+ _("Employee {0} does not belong to the company {1}").format(d.to_employee, self.company)
)
def on_submit(self):
diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js
index 0a316bd2974..cf3506a67d3 100644
--- a/erpnext/buying/doctype/supplier/supplier.js
+++ b/erpnext/buying/doctype/supplier/supplier.js
@@ -133,7 +133,10 @@ frappe.ui.form.on("Supplier", {
__("Actions")
);
- if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) {
+ if (
+ cint(frappe.defaults.get_default("enable_common_party_accounting")) &&
+ frappe.model.can_create("Party Link")
+ ) {
frm.add_custom_button(
__("Link with Customer"),
function () {
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 8c2b4db3fc9..3b5dbf2ee81 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -957,7 +957,9 @@ class StockController(AccountsController):
from erpnext.stock.stock_ledger import make_sl_entries
make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher)
- update_batch_qty(self.doctype, self.name, via_landed_cost_voucher=via_landed_cost_voucher)
+ update_batch_qty(
+ self.doctype, self.name, self.docstatus, via_landed_cost_voucher=via_landed_cost_voucher
+ )
def make_gl_entries_on_cancel(self, from_repost=False):
if not from_repost:
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 2f189ba4c46..2d67c7bd490 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -116,20 +116,6 @@ frappe.ui.form.on("Work Order", {
frm.set_indicator_formatter("operation", function (doc) {
return frm.doc.qty == doc.completed_qty ? "green" : "orange";
});
-
- if (frm.doc.docstatus == 0 && frm.doc.bom_no) {
- frappe.call({
- method: "erpnext.manufacturing.doctype.work_order.work_order.check_if_scrap_warehouse_mandatory",
- args: {
- bom_no: frm.doc.bom_no,
- },
- callback: function (r) {
- if (r.message["set_scrap_wh_mandatory"]) {
- frm.toggle_reqd("scrap_warehouse", true);
- }
- },
- });
- }
},
onload: function (frm) {
@@ -144,6 +130,20 @@ frappe.ui.form.on("Work Order", {
});
erpnext.work_order.set_default_warehouse(frm);
}
+
+ if (frm.doc.docstatus == 0 && frm.doc.bom_no) {
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.work_order.work_order.check_if_scrap_warehouse_mandatory",
+ args: {
+ bom_no: frm.doc.bom_no,
+ },
+ callback: function (r) {
+ if (r.message["set_scrap_wh_mandatory"]) {
+ frm.toggle_reqd("scrap_warehouse", true);
+ }
+ },
+ });
+ }
},
source_warehouse: function (frm) {
diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js
index aa8bea55094..598452276cc 100644
--- a/erpnext/selling/doctype/customer/customer.js
+++ b/erpnext/selling/doctype/customer/customer.js
@@ -175,7 +175,10 @@ frappe.ui.form.on("Customer", {
__("Actions")
);
- if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) {
+ if (
+ cint(frappe.defaults.get_default("enable_common_party_accounting")) &&
+ frappe.model.can_create("Party Link")
+ ) {
frm.add_custom_button(
__("Link with Supplier"),
function () {
diff --git a/erpnext/selling/report/address_and_contacts/address_and_contacts.py b/erpnext/selling/report/address_and_contacts/address_and_contacts.py
index 5d0e706930f..65fef8b5ac6 100644
--- a/erpnext/selling/report/address_and_contacts/address_and_contacts.py
+++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.py
@@ -3,10 +3,12 @@
import frappe
+from frappe import _
field_map = {
- "Contact": ["first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact"],
+ "Contact": ["name", "first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact"],
"Address": [
+ "name",
"address_line1",
"address_line2",
"city",
@@ -29,6 +31,12 @@ def get_columns(filters):
return [
f"{party_type}:Link/{party_type}",
f"{frappe.unscrub(str(party_type_value))}::150",
+ {
+ "label": _("Address"),
+ "fieldtype": "Link",
+ "options": "Address",
+ "hidden": 1,
+ },
"Address Line 1",
"Address Line 2",
"Postal Code",
@@ -36,6 +44,7 @@ def get_columns(filters):
"State",
"Country",
"Is Primary Address:Check",
+ {"label": _("Contact"), "fieldtype": "Link", "options": "Contact", "hidden": 1},
"First Name",
"Last Name",
"Phone",
diff --git a/erpnext/setup/demo.py b/erpnext/setup/demo.py
index 68d9fdfec5c..b21fb96546a 100644
--- a/erpnext/setup/demo.py
+++ b/erpnext/setup/demo.py
@@ -75,7 +75,7 @@ def create_demo_company():
frappe.db.set_single_value("Global Defaults", "demo_company", new_company.name)
frappe.db.set_default("company", new_company.name)
- bank_account = create_bank_account({"company_name": new_company.name})
+ bank_account = create_bank_account({"company_name": new_company.name}, demo=True)
frappe.db.set_value("Company", new_company.name, "default_bank_account", bank_account.name)
return new_company.name
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index c78e9b5b60d..0f3356ffa50 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -513,9 +513,11 @@ def update_stock_settings():
stock_settings.save()
-def create_bank_account(args):
+def create_bank_account(args, demo=False):
if not args.get("bank_account"):
- args["bank_account"] = _("Bank Account")
+ if not demo:
+ return
+ args["bank_account"] = _("Demo Bank Account")
company_name = args.get("company_name")
bank_account_group = frappe.db.get_value(
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
index 02ecc11f44a..0a24bfb5645 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
@@ -108,7 +108,8 @@
"in_list_view": 1,
"label": "Voucher Type",
"options": "DocType",
- "reqd": 1
+ "reqd": 1,
+ "search_index": 1
},
{
"fieldname": "voucher_no",
@@ -195,8 +196,7 @@
"fieldtype": "Select",
"label": "Type of Transaction",
"options": "\nInward\nOutward\nMaintenance\nAsset Repair",
- "reqd": 1,
- "search_index": 1
+ "reqd": 1
},
{
"default": "0",
@@ -256,7 +256,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2025-02-17 18:22:36.056205",
+ "modified": "2025-09-15 14:37:26.441742",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial and Batch Bundle",
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 e7d7446cea6..cf46867d64e 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
@@ -118,8 +118,8 @@ class SerialandBatchBundle(Document):
self.allow_existing_serial_nos()
if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test:
self.validate_serial_nos_duplicate()
- self.check_future_entries_exists()
+ self.check_future_entries_exists()
self.set_is_outward()
self.calculate_total_qty()
self.set_warehouse()
@@ -228,7 +228,7 @@ class SerialandBatchBundle(Document):
return
if self.voucher_type == "Stock Reconciliation":
- serial_nos = self.get_serial_nos_for_validate()
+ serial_nos, batches = self.get_serial_nos_for_validate()
else:
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
@@ -716,15 +716,22 @@ class SerialandBatchBundle(Document):
if self.flags and self.flags.via_landed_cost_voucher:
return
- if not self.has_serial_no:
- return
+ serial_nos = []
+ batches = []
if self.voucher_type == "Stock Reconciliation":
- serial_nos = self.get_serial_nos_for_validate(is_cancelled=is_cancelled)
+ serial_nos, batches = self.get_serial_nos_for_validate(is_cancelled=is_cancelled)
else:
+ batches = [d.batch_no for d in self.entries if d.batch_no]
+
+ if (
+ self.voucher_type != "Stock Reconciliation"
+ and not self.flags.ignore_validate_serial_batch
+ and self.has_serial_no
+ ):
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
- if not serial_nos:
+ if self.has_batch_no and not self.has_serial_no and not batches:
return
parent = frappe.qb.DocType("Serial and Batch Bundle")
@@ -740,65 +747,117 @@ class SerialandBatchBundle(Document):
.on(parent.name == child.parent)
.select(
child.serial_no,
+ child.batch_no,
parent.voucher_type,
parent.voucher_no,
)
.where(
- (child.serial_no.isin(serial_nos))
- & (child.parent != self.name)
+ (child.parent != self.name)
& (parent.item_code == self.item_code)
& (parent.docstatus == 1)
& (parent.is_cancelled == 0)
& (parent.type_of_transaction.isin(["Inward", "Outward"]))
)
.where(timestamp_condition)
- ).run(as_dict=True)
+ )
+
+ if self.has_batch_no and not self.has_serial_no:
+ future_entries = future_entries.where(parent.voucher_type == "Stock Reconciliation")
+
+ if serial_nos:
+ future_entries = future_entries.where(
+ (child.serial_no.isin(serial_nos))
+ | ((parent.warehouse == self.warehouse) & (parent.voucher_type == "Stock Reconciliation"))
+ )
+ elif self.has_serial_no:
+ future_entries = future_entries.where(
+ (parent.warehouse == self.warehouse) & (parent.voucher_type == "Stock Reconciliation")
+ )
+ elif batches:
+ future_entries = future_entries.where(child.batch_no.isin(batches))
+
+ future_entries = future_entries.run(as_dict=True)
if future_entries:
- msg = """The serial nos has been used in the future
- transactions so you need to cancel them first.
- The list of serial nos and their respective
- transactions are as below."""
+ if self.has_serial_no:
+ title = "Serial No Exists In Future Transaction(s)"
+ else:
+ title = "Batches Exists In Future Transaction(s)"
+
+ msg = """Since the stock reconciliation exists
+ for future dates, cancel it first. For Serial/Batch,
+ if you want to make a backdated transaction,
+ avoid using stock reconciliation.
+ For more details about the transaction,
+ please refer to the list below.
+ """
msg += "