diff --git a/.github/workflows/server-tests-mariadb-faux.yml b/.github/workflows/server-tests-mariadb-faux.yml
index 8df315c13ae..555ce260406 100644
--- a/.github/workflows/server-tests-mariadb-faux.yml
+++ b/.github/workflows/server-tests-mariadb-faux.yml
@@ -7,6 +7,7 @@ on:
paths:
- "**.js"
- "**.css"
+ - "**.svg"
- "**.md"
- "**.html"
- 'crowdin.yml'
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json
index 855feea6920..f569938dfc5 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json
@@ -33,6 +33,17 @@
},
"account_number": "1151.000"
},
+ "Pajak Dibayar di Muka": {
+ "PPN Masukan": {
+ "account_number": "1152.001",
+ "account_type": "Tax"
+ },
+ "PPh 23 Dibayar di Muka": {
+ "account_number": "1152.002",
+ "account_type": "Tax"
+ },
+ "account_number": "1152.000"
+ },
"account_number": "1150.000"
},
"Kas": {
@@ -97,17 +108,6 @@
},
"account_number": "1130.000"
},
- "Pajak Dibayar di Muka": {
- "PPN Masukan": {
- "account_number": "1151.001",
- "account_type": "Tax"
- },
- "PPh 23 Dibayar di Muka": {
- "account_number": "1152.001",
- "account_type": "Tax"
- },
- "account_number": "1150.000"
- },
"account_number": "1100.000"
},
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json
index 2465948c5ef..29e6c1fb638 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.json
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json
@@ -9,6 +9,7 @@
"engine": "InnoDB",
"field_order": [
"entry_type_and_date",
+ "company",
"is_system_generated",
"title",
"voucher_type",
@@ -17,7 +18,6 @@
"reversal_of",
"column_break1",
"from_template",
- "company",
"posting_date",
"finance_book",
"apply_tds",
@@ -638,7 +638,7 @@
"table_fieldname": "payment_entries"
}
],
- "modified": "2025-11-13 17:54:14.542903",
+ "modified": "2026-02-03 14:40:39.944524",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 4ea077f5d8c..6428896acae 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -74,8 +74,8 @@ class JournalEntry(AccountsController):
mode_of_payment: DF.Link | None
multi_currency: DF.Check
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
- party_not_required: DF.Check
override_tax_withholding_entries: DF.Check
+ party_not_required: DF.Check
pay_to_recd_from: DF.Data | None
payment_order: DF.Link | None
periodic_entry_difference_account: DF.Link | None
@@ -1691,6 +1691,10 @@ def get_exchange_rate(
credit=None,
exchange_rate=None,
):
+ # Ensure exchange_rate is always numeric to avoid calculation errors
+ if isinstance(exchange_rate, str):
+ exchange_rate = flt(exchange_rate) or 1
+
account_details = frappe.get_cached_value(
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
)
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index c3743c6e1f0..15ec96a5e9c 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -1450,16 +1450,15 @@ frappe.ui.form.on("Payment Entry", {
callback: function (r) {
if (!r.exc && r.message) {
// set taxes table
- if (r.message) {
- for (let tax of r.message) {
- if (tax.charge_type === "On Net Total") {
- tax.charge_type = "On Paid Amount";
- }
- frm.add_child("taxes", tax);
+ let taxes = r.message;
+ taxes.forEach((tax) => {
+ if (tax.charge_type === "On Net Total") {
+ tax.charge_type = "On Paid Amount";
}
- frm.events.apply_taxes(frm);
- frm.events.set_unallocated_amount(frm);
- }
+ });
+ frm.set_value("taxes", taxes);
+ frm.events.apply_taxes(frm);
+ frm.events.set_unallocated_amount(frm);
}
},
});
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index 6dae8dd22d5..9750e4c7f83 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -897,6 +897,53 @@ class TestPOSInvoice(IntegrationTestCase):
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
self.assertEqual(batch.qty, 5)
+ def test_pos_batch_reservation_with_return_qty(self):
+ """
+ Test POS Invoice reserved qty for batch without bundle with return invoices.
+ """
+ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+ get_auto_batch_nos,
+ )
+ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+ create_batch_item_with_batch,
+ )
+
+ create_batch_item_with_batch("_Batch Item Reserve Return", "TestBatch-RR 01")
+ se = make_stock_entry(
+ target="_Test Warehouse - _TC",
+ item_code="_Batch Item Reserve Return",
+ qty=30,
+ basic_rate=100,
+ )
+
+ se.reload()
+
+ batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
+
+ # POS Invoice for the batch without bundle
+ pos_inv = create_pos_invoice(item="_Batch Item Reserve Return", rate=300, qty=15, do_not_save=1)
+ pos_inv.append(
+ "payments",
+ {"mode_of_payment": "Cash", "amount": 4500},
+ )
+ pos_inv.items[0].batch_no = batch_no
+ pos_inv.save()
+ pos_inv.submit()
+
+ # POS Invoice return
+ pos_return = make_sales_return(pos_inv.name)
+
+ pos_return.insert()
+ pos_return.submit()
+
+ batches = get_auto_batch_nos(
+ frappe._dict({"item_code": "_Batch Item Reserve Return", "warehouse": "_Test Warehouse - _TC"})
+ )
+
+ for batch in batches:
+ if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
+ self.assertEqual(batch.qty, 30)
+
def test_pos_batch_item_qty_validation(self):
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
BatchNegativeStockError,
diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
index f43ee3c4ce2..23ef5678e0d 100644
--- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
+++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
@@ -98,8 +98,7 @@ def get_customers_list(pos_profile=None):
return (
frappe.db.sql(
- f""" select name, customer_name, customer_group,
- territory, customer_pos_id from tabCustomer where disabled = 0
+ f""" select name, customer_name, customer_group, territory from tabCustomer where disabled = 0
and {cond}""",
tuple(customer_groups),
as_dict=1,
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 092081308c9..4f6741f17cd 100644
--- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
@@ -415,8 +415,9 @@ def reconcile(doc: None | str = None) -> None:
for x in allocations:
pr.append("allocation", x)
+ skip_ref_details_update_for_pe = check_multi_currency(pr)
# reconcile
- pr.reconcile_allocations(skip_ref_details_update_for_pe=True)
+ pr.reconcile_allocations(skip_ref_details_update_for_pe=skip_ref_details_update_for_pe)
# If Payment Entry, update details only for newly linked references
# This is for performance
@@ -504,6 +505,37 @@ def reconcile(doc: None | str = None) -> None:
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
+def check_multi_currency(pr_doc):
+ GL = frappe.qb.DocType("GL Entry")
+ Account = frappe.qb.DocType("Account")
+
+ def get_account_currency(voucher_type, voucher_no):
+ currency = (
+ frappe.qb.from_(GL)
+ .join(Account)
+ .on(GL.account == Account.name)
+ .select(Account.account_currency)
+ .where(
+ (GL.voucher_type == voucher_type)
+ & (GL.voucher_no == voucher_no)
+ & (Account.account_type.isin(["Payable", "Receivable"]))
+ )
+ .limit(1)
+ ).run(as_dict=True)
+
+ return currency[0].account_currency if currency else None
+
+ for allocation in pr_doc.allocation:
+ reference_currency = get_account_currency(allocation.reference_type, allocation.reference_name)
+
+ invoice_currency = get_account_currency(allocation.invoice_type, allocation.invoice_number)
+
+ if reference_currency != invoice_currency:
+ return True
+
+ return False
+
+
@frappe.whitelist()
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
running_doc = None
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 980eee2c48c..02371e778cf 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -8,12 +8,12 @@
"email_append_to": 1,
"engine": "InnoDB",
"field_order": [
+ "company",
"title",
"naming_series",
"supplier",
"supplier_name",
"tax_id",
- "company",
"column_break_6",
"posting_date",
"posting_time",
@@ -1668,7 +1668,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2026-01-29 21:21:53.051193",
+ "modified": "2026-02-03 14:23:47.937128",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 888dbd59438..801820fbdb5 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -36,7 +36,7 @@ from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.buying.utils import check_on_hold_or_closed_status
-from erpnext.controllers.accounts_controller import validate_account_head
+from erpnext.controllers.accounts_controller import merge_taxes, validate_account_head
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
update_billed_amount_based_on_po,
@@ -2005,9 +2005,17 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
args = json.loads(args)
def post_parent_process(source_parent, target_parent):
- for row in target_parent.get("items"):
- if row.get("qty") == 0:
- target_parent.remove(row)
+ remove_items_with_zero_qty(target_parent)
+ set_missing_values(source_parent, target_parent)
+
+ def remove_items_with_zero_qty(target_parent):
+ target_parent.items = [row for row in target_parent.get("items") if row.get("qty") != 0]
+
+ def set_missing_values(source_parent, target_parent):
+ target_parent.run_method("set_missing_values")
+ if args and args.get("merge_taxes"):
+ merge_taxes(source_parent, target_parent)
+ target_parent.run_method("calculate_taxes_and_totals")
def update_item(obj, target, source_parent):
from erpnext.controllers.sales_and_purchase_return import get_returned_qty_map_for_row
@@ -2059,7 +2067,11 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
"postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
},
- "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
+ "Purchase Taxes and Charges": {
+ "doctype": "Purchase Taxes and Charges",
+ "reset_value": not (args and args.get("merge_taxes")),
+ "ignore": args.get("merge_taxes") if args else 0,
+ },
},
target_doc,
post_parent_process,
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 0ba6feef6da..6682270b4c9 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -44,6 +44,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
"Unreconcile Payment Entries",
"Serial and Batch Bundle",
"Bank Transaction",
+ "Packing Slip",
];
if (!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 6e82c3896c3..697804499f6 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -8,12 +8,12 @@
"engine": "InnoDB",
"field_order": [
"customer_section",
+ "company",
+ "company_tax_id",
"naming_series",
"customer",
"customer_name",
"tax_id",
- "company",
- "company_tax_id",
"column_break1",
"posting_date",
"posting_time",
diff --git a/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html b/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html
index 8a6968ee373..ad8c908dbcf 100644
--- a/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html
+++ b/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html
@@ -17,7 +17,7 @@
- | Date: | {{ frappe.utils.format_date(doc.creation) }} |
+ | Date: | {{ frappe.utils.format_date(doc.posting_date) }} |
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index d2fe570fa3b..a6dc46cabd9 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -176,7 +176,9 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
column_names = get_column_names()
# to display item as Item Code: Item Name
- columns[0] = "Sales Invoice:Link/Item:300"
+ columns[0]["fieldname"] = "sales_invoice"
+ columns[0]["options"] = "Item"
+ columns[0]["width"] = 300
# removing Item Code and Item Name columns
supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name")
customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name")
diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py
index c7585d9efd8..9cbdbee6316 100644
--- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py
+++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py
@@ -163,11 +163,11 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
- labels = [d.get("label") for d in columns[2:]]
+ labels = [d.get("label") for d in columns[4:]]
income_data, expense_data, net_profit = [], [], []
- for p in columns[2:]:
+ for p in columns[4:]:
if income:
income_data.append(income[-2].get(p.get("fieldname")))
if expense:
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index 05d03d47676..25a31ea698d 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -9,10 +9,9 @@
"engine": "InnoDB",
"field_order": [
"supplier_section",
+ "company",
"title",
"naming_series",
- "supplier",
- "supplier_name",
"order_confirmation_no",
"order_confirmation_date",
"get_items_from_open_material_requests",
@@ -21,8 +20,9 @@
"transaction_date",
"schedule_date",
"column_break1",
- "company",
+ "supplier",
"is_subcontracted",
+ "supplier_name",
"has_unit_price_items",
"supplier_warehouse",
"amended_from",
@@ -1310,7 +1310,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2026-01-29 21:22:54.323838",
+ "modified": "2026-02-03 14:44:55.192192",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 69593927e69..f78ea1b48c8 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -846,12 +846,34 @@ def get_filters(
if reference_voucher_detail_no:
filters["voucher_detail_no"] = reference_voucher_detail_no
- if voucher_type in ["Purchase Receipt", "Purchase Invoice"] and item_row and item_row.get("warehouse"):
- filters["warehouse"] = item_row.get("warehouse")
+ warehouses = []
+ if voucher_type in ["Purchase Receipt", "Purchase Invoice"] and item_row:
+ if reference_voucher_detail_no:
+ warehouses = get_warehouses_for_return(voucher_type, reference_voucher_detail_no)
+
+ if item_row.get("warehouse") and item_row.get("warehouse") in warehouses:
+ filters["warehouse"] = item_row.get("warehouse")
return filters
+def get_warehouses_for_return(voucher_type, name):
+ warehouses = []
+ warehouse_details = frappe.get_all(
+ voucher_type + " Item",
+ filters={"name": name, "docstatus": 1},
+ fields=["warehouse", "rejected_warehouse"],
+ )
+
+ for d in warehouse_details:
+ if d.warehouse:
+ warehouses.append(d.warehouse)
+ if d.rejected_warehouse:
+ warehouses.append(d.rejected_warehouse)
+
+ return warehouses
+
+
def get_returned_serial_nos(child_doc, parent_doc, serial_no_field=None, ignore_voucher_detail_no=None):
from erpnext.stock.doctype.serial_no.serial_no import (
get_serial_nos as get_serial_nos_from_serial_no,
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index db54db5cf1f..0f883eeb50f 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -296,7 +296,7 @@ class SellingController(StockController):
_(
"""Row #{0}: Selling rate for item {1} is lower than its {2}.
Selling {3} should be atleast {4}.
Alternatively,
- you can disable selling price validation in {5} to bypass
+ you can disable '{5}' in {6} to bypass
this validation."""
).format(
idx,
@@ -304,7 +304,8 @@ class SellingController(StockController):
bold(ref_rate_field),
bold("net rate"),
bold(rate),
- get_link_to_form("Selling Settings", "Selling Settings"),
+ bold(frappe.get_meta("Selling Settings").get_label("validate_selling_price")),
+ get_link_to_form("Selling Settings"),
),
title=_("Invalid Selling Price"),
)
@@ -313,7 +314,6 @@ class SellingController(StockController):
return
is_internal_customer = self.get("is_internal_customer")
- valuation_rate_map = {}
for item in self.items:
if not item.item_code or item.is_free_item:
@@ -323,7 +323,9 @@ class SellingController(StockController):
"Item", item.item_code, ("last_purchase_rate", "is_stock_item")
)
- last_purchase_rate_in_sales_uom = last_purchase_rate * (item.conversion_factor or 1)
+ last_purchase_rate_in_sales_uom = flt(
+ last_purchase_rate * (item.conversion_factor or 1), item.precision("base_net_rate")
+ )
if flt(item.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
throw_message(item.idx, item.item_name, last_purchase_rate_in_sales_uom, "last purchase rate")
@@ -331,50 +333,16 @@ class SellingController(StockController):
if is_internal_customer or not is_stock_item:
continue
- valuation_rate_map[(item.item_code, item.warehouse)] = None
-
- if not valuation_rate_map:
- return
-
- or_conditions = (
- f"""(item_code = {frappe.db.escape(valuation_rate[0])}
- and warehouse = {frappe.db.escape(valuation_rate[1])})"""
- for valuation_rate in valuation_rate_map
- )
-
- valuation_rates = frappe.db.sql(
- f"""
- select
- item_code, warehouse, valuation_rate
- from
- `tabBin`
- where
- ({" or ".join(or_conditions)})
- and valuation_rate > 0
- """,
- as_dict=True,
- )
-
- for rate in valuation_rates:
- valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate
-
- for item in self.items:
- if not item.item_code or item.is_free_item:
- continue
-
- last_valuation_rate = valuation_rate_map.get((item.item_code, item.warehouse))
-
- if not last_valuation_rate:
- continue
-
- last_valuation_rate_in_sales_uom = last_valuation_rate * (item.conversion_factor or 1)
-
- if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom):
+ if item.get("incoming_rate") and item.base_net_rate < (
+ valuation_rate := flt(
+ item.incoming_rate * (item.conversion_factor or 1), item.precision("base_net_rate")
+ )
+ ):
throw_message(
item.idx,
item.item_name,
- last_valuation_rate_in_sales_uom,
- "valuation rate (Moving Average)",
+ valuation_rate,
+ "valuation rate",
)
def get_item_list(self):
@@ -533,6 +501,8 @@ class SellingController(StockController):
if self.doctype not in ("Delivery Note", "Sales Invoice"):
return
+ from erpnext.stock.serial_batch_bundle import get_batch_nos
+
allow_at_arms_length_price = frappe.get_cached_value(
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
)
@@ -540,6 +510,7 @@ class SellingController(StockController):
"Selling Settings", "set_zero_rate_for_expired_batch"
)
+ old_doc = self.get_doc_before_save()
items = self.get("items") + (self.get("packed_items") or [])
for d in items:
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
@@ -569,6 +540,29 @@ class SellingController(StockController):
# Get incoming rate based on original item cost based on valuation method
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
+ if old_doc:
+ old_item = next(
+ (
+ item
+ for item in (old_doc.get("items") + (old_doc.get("packed_items") or []))
+ if item.name == d.name
+ ),
+ None,
+ )
+ if old_item:
+ old_qty = flt(
+ old_item.get("stock_qty") or old_item.get("actual_qty") or old_item.get("qty")
+ )
+ if (
+ old_item.item_code != d.item_code
+ or old_item.warehouse != d.warehouse
+ or old_qty != qty
+ or old_item.batch_no != d.batch_no
+ or get_batch_nos(old_item.serial_and_batch_bundle)
+ != get_batch_nos(d.serial_and_batch_bundle)
+ ):
+ d.incoming_rate = 0
+
if (
not d.incoming_rate
or self.is_internal_transfer()
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index 70eb459112f..b16c95722a6 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -91,7 +91,8 @@ status_map = {
],
"Delivery Note": [
["Draft", None],
- ["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
+ ["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"],
+ ["Partially Billed", "eval:self.per_billed < 100 and self.per_billed > 0 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index 473ab7c86b3..11ad4f4d2ef 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -313,10 +313,10 @@ class SubcontractingController(StockController):
):
for row in frappe.get_all(
f"{self.subcontract_data.order_doctype} Item",
- fields=["item_code", {"SUB": ["qty", "received_qty"], "as": "qty"}, "parent", "name"],
+ fields=["item_code", {"SUB": ["qty", "received_qty"], "as": "qty"}, "parent", "bom"],
filters={"docstatus": 1, "parent": ("in", self.subcontract_orders)},
):
- self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
+ self.qty_to_be_received[(row.item_code, row.parent, row.bom)] += row.qty
def __get_transferred_items(self):
se = frappe.qb.DocType("Stock Entry")
@@ -922,13 +922,17 @@ class SubcontractingController(StockController):
self.__set_serial_nos(item_row, rm_obj)
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
- key = (item_row.item_code, item_row.get(self.subcontract_data.order_field))
+ key = (
+ item_row.item_code,
+ item_row.get(self.subcontract_data.order_field),
+ item_row.get("bom"),
+ )
if self.qty_to_be_received == item_row.qty:
return transfer_item.qty
- if self.qty_to_be_received:
- qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
+ if self.qty_to_be_received.get(key):
+ qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key))
transfer_item.item_details.required_qty = transfer_item.qty
if transfer_item.serial_no or frappe.get_cached_value(
@@ -977,7 +981,11 @@ class SubcontractingController(StockController):
if self.qty_to_be_received:
self.qty_to_be_received[
- (row.item_code, row.get(self.subcontract_data.order_field))
+ (
+ row.item_code,
+ row.get(self.subcontract_data.order_field),
+ row.get("bom"),
+ )
] -= row.qty
def __set_rate_for_serial_and_batch_bundle(self):
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 3fdd00237e7..e3235379127 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -461,3 +461,4 @@ erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges
execute:frappe.delete_doc_if_exists("Workspace Sidebar", "Opening & Closing")
erpnext.patches.v16_0.migrate_transaction_deletion_task_flags_to_status # 2
erpnext.patches.v16_0.set_ordered_qty_in_quotation_item
+erpnext.patches.v16_0.update_company_custom_field_in_bin
\ No newline at end of file
diff --git a/erpnext/patches/v16_0/update_company_custom_field_in_bin.py b/erpnext/patches/v16_0/update_company_custom_field_in_bin.py
new file mode 100644
index 00000000000..e0b36d91fd7
--- /dev/null
+++ b/erpnext/patches/v16_0/update_company_custom_field_in_bin.py
@@ -0,0 +1,14 @@
+import frappe
+
+
+def execute():
+ frappe.reload_doc("stock", "doctype", "bin")
+
+ frappe.db.sql(
+ """
+ UPDATE `tabBin` b
+ INNER JOIN `tabWarehouse` w ON b.warehouse = w.name
+ SET b.company = w.company
+ WHERE b.company IS NULL OR b.company = ''
+ """
+ )
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 20a421195eb..d9b025a7f92 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -308,6 +308,8 @@ class Project(Document):
self.gross_margin = flt(self.total_billed_amount) - expense_amount
if self.total_billed_amount:
self.per_gross_margin = (self.gross_margin / flt(self.total_billed_amount)) * 100
+ else:
+ self.per_gross_margin = 0
def update_purchase_costing(self):
total_purchase_cost = calculate_total_purchase_cost(self.name)
diff --git a/erpnext/public/icons/desktop_icons/solid/account_setup.svg b/erpnext/public/icons/desktop_icons/solid/accounts_setup.svg
similarity index 100%
rename from erpnext/public/icons/desktop_icons/solid/account_setup.svg
rename to erpnext/public/icons/desktop_icons/solid/accounts_setup.svg
diff --git a/erpnext/public/icons/desktop_icons/subtle/account_setup.svg b/erpnext/public/icons/desktop_icons/subtle/accounts_setup.svg
similarity index 100%
rename from erpnext/public/icons/desktop_icons/subtle/account_setup.svg
rename to erpnext/public/icons/desktop_icons/subtle/accounts_setup.svg
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 4ae844c6116..d4a4577d8a0 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -625,6 +625,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
callback: function (r) {
if (!r.exc) {
me.frm.refresh_fields();
+ me.show_batch_dialog_if_required(item);
}
},
});
@@ -635,26 +636,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
process_item_selection(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
+ let update_stock = 0;
var me = this;
- 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
item.conversion_factor = 0;
-
- if (["Sales Invoice", "Purchase Invoice"].includes(this.frm.doc.doctype)) {
- update_stock = cint(me.frm.doc.update_stock);
- show_batch_dialog = update_stock;
- } else if (this.frm.doc.doctype === "Purchase Receipt" || this.frm.doc.doctype === "Delivery Note") {
- show_batch_dialog = 1;
- }
-
- if (show_batch_dialog && item.use_serial_batch_fields === 1) {
- show_batch_dialog = 0;
- }
-
item.barcode = null;
if (item.item_code || item.serial_no) {
@@ -765,74 +753,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
},
() => me.toggle_conversion_factor(item),
- () => {
- if (show_batch_dialog && !frappe.flags.trigger_from_barcode_scanner)
- return frappe.db
- .get_value("Item", item.item_code, [
- "has_batch_no",
- "has_serial_no",
- ])
- .then((r) => {
- if (
- r.message &&
- (r.message.has_batch_no || r.message.has_serial_no)
- ) {
- frappe.flags.hide_serial_batch_dialog = false;
- } else {
- show_batch_dialog = false;
- }
- });
- },
- () => {
- // check if batch serial selector is disabled or not
- if (show_batch_dialog && !frappe.flags.hide_serial_batch_dialog)
- return frappe.db
- .get_single_value(
- "Stock Settings",
- "disable_serial_no_and_batch_selector"
- )
- .then((value) => {
- if (value) {
- frappe.flags.hide_serial_batch_dialog = true;
- }
- });
- },
- () => {
- if (
- show_batch_dialog &&
- !frappe.flags.hide_serial_batch_dialog &&
- !frappe.flags.dialog_set
- ) {
- var d = locals[cdt][cdn];
- $.each(r.message, function (k, v) {
- if (!d[k]) d[k] = v;
- });
-
- if (d.has_batch_no && d.has_serial_no) {
- d.batch_no = undefined;
- }
-
- frappe.flags.dialog_set = true;
- erpnext.show_serial_batch_selector(
- me.frm,
- d,
- (item) => {
- me.frm.script_manager.trigger("qty", item.doctype, item.name);
- if (!me.frm.doc.set_warehouse)
- me.frm.script_manager.trigger(
- "warehouse",
- item.doctype,
- item.name
- );
- me.apply_price_list(item, true);
- },
- undefined,
- !frappe.flags.hide_serial_batch_dialog
- );
- } else {
- frappe.flags.dialog_set = false;
- }
- },
+ () => me.show_batch_dialog_if_required(item),
() => me.conversion_factor(doc, cdt, cdn, true),
() => me.remove_pricing_rule(item),
() => {
@@ -853,6 +774,78 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
}
+ show_batch_dialog_if_required(item) {
+ let show_batch_dialog = 0;
+ let update_stock = 0;
+ let me = this;
+
+ if (!item.item_code) {
+ return;
+ }
+
+ if (["Sales Invoice", "Purchase Invoice"].includes(this.frm.doc.doctype)) {
+ update_stock = cint(me.frm.doc.update_stock);
+ show_batch_dialog = update_stock;
+ } else if (this.frm.doc.doctype === "Purchase Receipt" || this.frm.doc.doctype === "Delivery Note") {
+ show_batch_dialog = 1;
+ }
+
+ if (show_batch_dialog && item.use_serial_batch_fields === 1) {
+ show_batch_dialog = 0;
+ }
+
+ frappe.run_serially([
+ () => {
+ if (show_batch_dialog && !frappe.flags.trigger_from_barcode_scanner)
+ return frappe.db
+ .get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
+ .then((r) => {
+ if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
+ item.has_serial_no = r.message.has_serial_no;
+ item.has_batch_no = r.message.has_batch_no;
+ frappe.flags.hide_serial_batch_dialog = false;
+ } else {
+ show_batch_dialog = false;
+ }
+ });
+ },
+ () => {
+ // check if batch serial selector is disabled or not
+ if (show_batch_dialog && !frappe.flags.hide_serial_batch_dialog)
+ return frappe.db
+ .get_single_value("Stock Settings", "disable_serial_no_and_batch_selector")
+ .then((value) => {
+ if (value) {
+ frappe.flags.hide_serial_batch_dialog = true;
+ }
+ });
+ },
+ () => {
+ if (show_batch_dialog && !frappe.flags.hide_serial_batch_dialog && !frappe.flags.dialog_set) {
+ if (item.has_batch_no && item.has_serial_no) {
+ item.batch_no = undefined;
+ }
+
+ frappe.flags.dialog_set = true;
+ erpnext.show_serial_batch_selector(
+ me.frm,
+ item,
+ (item) => {
+ me.frm.script_manager.trigger("qty", item.doctype, item.name);
+ if (!me.frm.doc.set_warehouse)
+ me.frm.script_manager.trigger("warehouse", item.doctype, item.name);
+ me.apply_price_list(item, true);
+ },
+ undefined,
+ !frappe.flags.hide_serial_batch_dialog
+ );
+ } else {
+ frappe.flags.dialog_set = false;
+ }
+ },
+ ]);
+ }
+
price_list_rate(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]);
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index dff5f1e3ead..355dadbc534 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -994,7 +994,7 @@ erpnext.utils.map_current_doc = function (opts) {
if (opts.source_doctype) {
let data_fields = [];
- if (["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)) {
+ if (["Purchase Receipt", "Delivery Note", "Purchase Invoice"].includes(opts.source_doctype)) {
let target_meta = frappe.get_meta(cur_frm.doc.doctype);
if (target_meta.fields.find((f) => f.fieldname === "taxes")) {
data_fields.push({
diff --git a/erpnext/public/js/utils/contact_address_quick_entry.js b/erpnext/public/js/utils/contact_address_quick_entry.js
index 1a1d67b475a..f6ce5d0d28b 100644
--- a/erpnext/public/js/utils/contact_address_quick_entry.js
+++ b/erpnext/public/js/utils/contact_address_quick_entry.js
@@ -110,12 +110,6 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm
options: "Country",
mandatory_depends_on: "eval:doc.city || doc.address_line1",
},
- {
- label: __("Customer POS Id"),
- fieldname: "customer_pos_id",
- fieldtype: "Data",
- hidden: 1,
- },
];
return variant_fields;
diff --git a/erpnext/regional/italy/utils.py b/erpnext/regional/italy/utils.py
index 2e8d38bebde..93e9b60f5a8 100644
--- a/erpnext/regional/italy/utils.py
+++ b/erpnext/regional/italy/utils.py
@@ -142,7 +142,14 @@ def download_zip(files, output_filename):
def get_invoice_summary(items, taxes, item_wise_tax_details):
summary_data = frappe._dict()
- taxes_wise_tax_details = {d.tax_row: d for d in item_wise_tax_details}
+ taxes_wise_tax_details = {}
+
+ for d in item_wise_tax_details:
+ if d.tax_row not in taxes_wise_tax_details:
+ taxes_wise_tax_details[d.tax_row] = []
+
+ taxes_wise_tax_details[d.tax_row].append(d)
+
for tax in taxes:
# Include only VAT charges.
if tax.charge_type == "Actual":
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 500fe723fd0..e306814027d 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -113,12 +113,37 @@ class Customer(TransactionBase):
def get_customer_name(self):
self.customer_name = self.customer_name.strip()
if frappe.db.get_value("Customer", self.customer_name) and not frappe.flags.in_import:
- count = frappe.db.sql(
- """select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabCustomer
- where name like %s""",
- f"%{self.customer_name} - %",
- as_list=1,
- )[0][0]
+ name_prefix = f"{self.customer_name} - %"
+
+ if frappe.db.db_type == "postgres":
+ # Postgres: extract trailing digits (e.g. "Customer - 3") and cast to int.
+ # NOTE: PostgreSQL is strict about types; MySQL's UNSIGNED cast does not exist.
+ count = frappe.db.sql(
+ """
+ SELECT COALESCE(
+ MAX(CAST(SUBSTRING(name FROM '\\d+$') AS INTEGER)),
+ 0
+ )
+ FROM tabCustomer
+ WHERE name LIKE %(name_prefix)s
+ """,
+ {"name_prefix": name_prefix},
+ as_list=1,
+ )[0][0]
+ else:
+ # MariaDB/MySQL: keep existing behavior.
+ count = frappe.db.sql(
+ """
+ SELECT COALESCE(
+ MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)),
+ 0
+ )
+ FROM tabCustomer
+ WHERE name LIKE %(name_prefix)s
+ """,
+ {"name_prefix": name_prefix},
+ as_list=1,
+ )[0][0]
count = cint(count) + 1
new_customer_name = f"{self.customer_name} - {cstr(count)}"
@@ -506,6 +531,9 @@ def _set_missing_values(source, target):
if contact:
target.contact_person = contact[0].parent
+ target.contact_display, target.contact_email, target.contact_mobile = frappe.get_value(
+ "Contact", contact[0].parent, ["full_name", "email_id", "mobile_no"]
+ )
@frappe.whitelist()
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index de17b04c450..f8ef775e9e4 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -11,18 +11,18 @@
"field_order": [
"customer_section",
"column_break0",
+ "company",
"naming_series",
- "customer",
- "customer_name",
- "tax_id",
"order_type",
"column_break_7",
"transaction_date",
"delivery_date",
"column_break1",
+ "customer",
+ "customer_name",
+ "tax_id",
"po_no",
"po_date",
- "company",
"skip_delivery_note",
"has_unit_price_items",
"is_subcontracted",
@@ -1713,7 +1713,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2026-01-29 21:23:48.362401",
+ "modified": "2026-02-03 14:45:50.314361",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json
index 7de1aa9d189..b0668896597 100644
--- a/erpnext/stock/doctype/bin/bin.json
+++ b/erpnext/stock/doctype/bin/bin.json
@@ -22,6 +22,7 @@
"reserved_stock",
"section_break_pmrs",
"stock_uom",
+ "company",
"column_break_0slj",
"valuation_rate",
"stock_value"
@@ -132,6 +133,14 @@
"options": "UOM",
"read_only": 1
},
+ {
+ "fetch_from": "warehouse.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "read_only": 1
+ },
{
"fieldname": "valuation_rate",
"fieldtype": "Float",
@@ -186,7 +195,7 @@
"idx": 1,
"in_create": 1,
"links": [],
- "modified": "2024-03-27 13:06:39.414036",
+ "modified": "2026-02-01 08:11:46.824913",
"modified_by": "Administrator",
"module": "Stock",
"name": "Bin",
@@ -231,8 +240,9 @@
}
],
"quick_entry": 1,
+ "row_format": "Dynamic",
"search_fields": "item_code,warehouse",
"sort_field": "creation",
"sort_order": "ASC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 62c4528f432..ae1c44d6419 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -19,6 +19,7 @@ class Bin(Document):
from frappe.types import DF
actual_qty: DF.Float
+ company: DF.Link | None
indented_qty: DF.Float
item_code: DF.Link
ordered_qty: DF.Float
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index 52392e96866..9abe8c8c409 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -1070,7 +1070,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
- "options": "\nDraft\nTo Bill\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed",
+ "options": "\nDraft\nTo Bill\nPartially Billed\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed",
"print_hide": 1,
"print_width": "150px",
"read_only": 1,
@@ -1434,7 +1434,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2026-01-29 21:24:11.781261",
+ "modified": "2026-02-03 12:27:19.055918",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 3b52f91b492..07c1623a182 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -127,7 +127,15 @@ class DeliveryNote(SellingController):
shipping_address_name: DF.Link | None
shipping_rule: DF.Link | None
status: DF.Literal[
- "", "Draft", "To Bill", "Completed", "Return", "Return Issued", "Cancelled", "Closed"
+ "",
+ "Draft",
+ "To Bill",
+ "Partially Billed",
+ "Completed",
+ "Return",
+ "Return Issued",
+ "Cancelled",
+ "Closed",
]
tax_category: DF.Link | None
tax_id: DF.Data | None
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
index fccc401931e..56698ccf76b 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
@@ -18,8 +18,10 @@ frappe.listview_settings["Delivery Note"] = {
return [__("Closed"), "green", "status,=,Closed"];
} else if (doc.status === "Return Issued") {
return [__("Return Issued"), "grey", "status,=,Return Issued"];
- } else if (flt(doc.per_billed, 2) < 100) {
- return [__("To Bill"), "orange", "per_billed,<,100|docstatus,=,1"];
+ } else if (flt(doc.per_billed) == 0) {
+ return [__("To Bill"), "orange", "per_billed,=,0|docstatus,=,1"];
+ } else if (flt(doc.per_billed, 2) > 0 && flt(doc.per_billed, 2) < 100) {
+ return [__("Partially Billed"), "yellow", "per_billed,<,100|docstatus,=,1"];
} else if (flt(doc.per_billed, 2) === 100) {
return [__("Completed"), "green", "per_billed,=,100|docstatus,=,1"];
}
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index adb75eea023..02713b6f917 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -1101,7 +1101,8 @@ class TestDeliveryNote(IntegrationTestCase):
self.assertEqual(dn2.get("items")[0].billed_amt, 400)
self.assertEqual(dn2.per_billed, 80)
- self.assertEqual(dn2.status, "To Bill")
+ # Since 20% of DN2 is yet to be billed, it should be classified as partially billed.
+ self.assertEqual(dn2.status, "Partially Billed")
def test_dn_billing_status_case4(self):
# SO -> SI -> DN
@@ -2863,6 +2864,23 @@ class TestDeliveryNote(IntegrationTestCase):
for entry in sabb.entries:
self.assertEqual(entry.incoming_rate, 200)
+ @IntegrationTestCase.change_settings("Selling Settings", {"validate_selling_price": 1})
+ def test_validate_selling_price(self):
+ item_code = make_item("VSP Item", properties={"is_stock_item": 1}).name
+ make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=10)
+ make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=1)
+
+ dn = create_delivery_note(
+ item_code=item_code,
+ qty=1,
+ rate=9,
+ do_not_save=True,
+ )
+ self.assertRaises(frappe.ValidationError, dn.save)
+ dn.items[0].incoming_rate = 0
+ dn.items[0].stock_qty = 2
+ dn.save()
+
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 5b7534d3247..7f23aeb16c4 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -5111,6 +5111,128 @@ class TestPurchaseReceipt(IntegrationTestCase):
self.assertEqual(stk_ledger.incoming_rate, 120)
self.assertEqual(stk_ledger.stock_value_difference, 600)
+ def test_negative_stock_error_for_purchase_return_when_stock_exists_in_future_date(self):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+ from erpnext.stock.stock_ledger import NegativeStockError
+
+ item_code = make_item(
+ "Test Negative Stock for Purchase Return with Future Stock Item",
+ {
+ "is_stock_item": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "TNSPFPRI.#####",
+ },
+ ).name
+
+ make_purchase_receipt(
+ item_code=item_code,
+ posting_date=add_days(today(), -4),
+ qty=100,
+ rate=100,
+ warehouse="_Test Warehouse - _TC",
+ )
+
+ pr1 = make_purchase_receipt(
+ item_code=item_code,
+ posting_date=add_days(today(), -3),
+ qty=100,
+ rate=100,
+ warehouse="_Test Warehouse - _TC",
+ )
+
+ batch1 = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle)
+
+ pr2 = make_purchase_receipt(
+ item_code=item_code,
+ posting_date=add_days(today(), -2),
+ qty=100,
+ rate=100,
+ warehouse="_Test Warehouse - _TC",
+ )
+
+ batch2 = get_batch_from_bundle(pr2.items[0].serial_and_batch_bundle)
+
+ make_stock_entry(
+ item_code=item_code,
+ qty=100,
+ posting_date=add_days(today(), -1),
+ source="_Test Warehouse - _TC",
+ target="_Test Warehouse 1 - _TC",
+ batch_no=batch1,
+ use_serial_batch_fields=1,
+ )
+
+ make_stock_entry(
+ item_code=item_code,
+ qty=100,
+ posting_date=add_days(today(), -1),
+ source="_Test Warehouse - _TC",
+ target="_Test Warehouse 1 - _TC",
+ batch_no=batch2,
+ use_serial_batch_fields=1,
+ )
+
+ make_stock_entry(
+ item_code=item_code,
+ qty=100,
+ posting_date=today(),
+ source="_Test Warehouse 1 - _TC",
+ target="_Test Warehouse - _TC",
+ batch_no=batch1,
+ use_serial_batch_fields=1,
+ )
+
+ make_purchase_entry = make_return_doc("Purchase Receipt", pr1.name)
+ make_purchase_entry.set_posting_time = 1
+ make_purchase_entry.posting_date = pr1.posting_date
+ self.assertRaises(NegativeStockError, make_purchase_entry.submit)
+
+ def test_purchase_return_from_different_warehouse(self):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
+ item_code = make_item(
+ "Test Purchase Return From Different Warehouse Item",
+ {
+ "is_stock_item": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "TPRFDWU.#####",
+ },
+ ).name
+
+ pr1 = make_purchase_receipt(
+ item_code=item_code,
+ posting_date=add_days(today(), -4),
+ qty=100,
+ rate=100,
+ warehouse="_Test Warehouse - _TC",
+ )
+
+ batch1 = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle)
+
+ make_stock_entry(
+ item_code=item_code,
+ qty=100,
+ posting_date=add_days(today(), -1),
+ source="_Test Warehouse - _TC",
+ target="_Test Warehouse 1 - _TC",
+ batch_no=batch1,
+ use_serial_batch_fields=1,
+ )
+
+ make_purchase_entry = make_return_doc("Purchase Receipt", pr1.name)
+ make_purchase_entry.items[0].warehouse = "_Test Warehouse 1 - _TC"
+ make_purchase_entry.submit()
+ make_purchase_entry.reload()
+
+ sabb = frappe.get_doc("Serial and Batch Bundle", make_purchase_entry.items[0].serial_and_batch_bundle)
+ for row in sabb.entries:
+ self.assertEqual(row.warehouse, "_Test Warehouse 1 - _TC")
+ self.assertEqual(row.incoming_rate, 100)
+
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
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 2a9bcd4edd6..bd1e6902da0 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
@@ -17,6 +17,7 @@ from frappe.utils import (
cint,
cstr,
flt,
+ get_datetime,
get_link_to_form,
getdate,
now,
@@ -438,6 +439,8 @@ class SerialandBatchBundle(Document):
)
def get_valuation_rate_for_return_entry(self, return_against):
+ from erpnext.controllers.sales_and_purchase_return import get_warehouses_for_return
+
if not self.voucher_detail_no:
return {}
@@ -467,9 +470,11 @@ class SerialandBatchBundle(Document):
["Serial and Batch Bundle", "voucher_detail_no", "=", return_against_voucher_detail_no],
]
+ # Added to handle rejected warehouse case
if self.voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
- # Added to handle rejected warehouse case
- filters.append(["Serial and Batch Entry", "warehouse", "=", self.warehouse])
+ warehouses = get_warehouses_for_return(self.voucher_type, return_against_voucher_detail_no)
+ if self.warehouse in warehouses:
+ filters.append(["Serial and Batch Entry", "warehouse", "=", self.warehouse])
bundle_data = frappe.get_all(
"Serial and Batch Bundle",
@@ -1451,31 +1456,44 @@ class SerialandBatchBundle(Document):
for d in self.entries:
available_qty = batch_wise_available_qty.get(d.batch_no, 0)
if flt(available_qty, precision) < 0:
- frappe.throw(
- _(
- """
- The Batch {0} of an item {1} has negative stock in the warehouse {2}. Please add a stock quantity of {3} to proceed with this entry."""
- ).format(
- bold(d.batch_no),
- bold(self.item_code),
- bold(self.warehouse),
- bold(abs(flt(available_qty, precision))),
- ),
- title=_("Negative Stock Error"),
- )
+ self.throw_negative_batch(d.batch_no, available_qty, precision)
+
+ def throw_negative_batch(self, batch_no, available_qty, precision):
+ from erpnext.stock.stock_ledger import NegativeStockError
+
+ frappe.throw(
+ _(
+ """
+ The Batch {0} of an item {1} has negative stock in the warehouse {2}. Please add a stock quantity of {3} to proceed with this entry."""
+ ).format(
+ bold(batch_no),
+ bold(self.item_code),
+ bold(self.warehouse),
+ bold(abs(flt(available_qty, precision))),
+ ),
+ title=_("Negative Stock Error"),
+ exc=NegativeStockError,
+ )
def get_batchwise_available_qty(self):
- available_qty = self.get_available_qty_from_sabb()
- available_qty_from_ledger = self.get_available_qty_from_stock_ledger()
+ batchwise_entries = self.get_available_qty_from_sabb()
+ batchwise_entries.extend(self.get_available_qty_from_stock_ledger())
- if not available_qty_from_ledger:
- return available_qty
+ available_qty = frappe._dict({})
+ batchwise_entries = sorted(
+ batchwise_entries,
+ key=lambda x: (get_datetime(x.get("posting_datetime")), get_datetime(x.get("creation"))),
+ )
- for batch_no, qty in available_qty_from_ledger.items():
- if batch_no in available_qty:
- available_qty[batch_no] += qty
+ precision = frappe.get_precision("Serial and Batch Entry", "qty")
+ for row in batchwise_entries:
+ if row.batch_no in available_qty:
+ available_qty[row.batch_no] += flt(row.qty)
else:
- available_qty[batch_no] = qty
+ available_qty[row.batch_no] = flt(row.qty)
+
+ if flt(available_qty[row.batch_no], precision) < 0:
+ self.throw_negative_batch(row.batch_no, available_qty[row.batch_no], precision)
return available_qty
@@ -1488,7 +1506,9 @@ class SerialandBatchBundle(Document):
frappe.qb.from_(sle)
.select(
sle.batch_no,
- Sum(sle.actual_qty).as_("available_qty"),
+ sle.actual_qty.as_("qty"),
+ sle.posting_datetime,
+ sle.creation,
)
.where(
(sle.item_code == self.item_code)
@@ -1500,12 +1520,9 @@ class SerialandBatchBundle(Document):
& (sle.batch_no.isnotnull())
)
.for_update()
- .groupby(sle.batch_no)
)
- res = query.run(as_list=True)
-
- return frappe._dict(res) if res else frappe._dict()
+ return query.run(as_dict=True)
def get_available_qty_from_sabb(self):
batches = [d.batch_no for d in self.entries if d.batch_no]
@@ -1516,7 +1533,9 @@ class SerialandBatchBundle(Document):
frappe.qb.from_(child)
.select(
child.batch_no,
- Sum(child.qty).as_("available_qty"),
+ child.qty,
+ child.posting_datetime,
+ child.creation,
)
.where(
(child.item_code == self.item_code)
@@ -1527,13 +1546,10 @@ class SerialandBatchBundle(Document):
& (child.type_of_transaction.isin(["Inward", "Outward"]))
)
.for_update()
- .groupby(child.batch_no)
)
query = query.where(child.voucher_type != "Pick List")
- res = query.run(as_list=True)
-
- return frappe._dict(res) if res else frappe._dict()
+ return query.run(as_dict=True)
def validate_voucher_no_docstatus(self):
if self.voucher_type == "POS Invoice":
@@ -2596,11 +2612,11 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
key = (row.batch_no, row.warehouse)
if key in pos_batches:
- pos_batches[key]["qty"] -= row.qty * -1 if row.is_return else row.qty
+ pos_batches[key]["qty"] += row.qty * -1
else:
pos_batches[key] = frappe._dict(
{
- "qty": (row.qty * -1 if not row.is_return else row.qty),
+ "qty": row.qty * -1,
"warehouse": row.warehouse,
}
)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 22ca84f6e38..e5b6a647724 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -1271,15 +1271,11 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig
for d in items:
if (d.item_code, d.warehouse) in itemwise_batch_data:
- valuation_rate = get_stock_balance(
- d.item_code, d.warehouse, posting_date, posting_time, with_valuation_rate=True
- )[1]
-
for row in itemwise_batch_data.get((d.item_code, d.warehouse)):
if ignore_empty_stock and not row.qty:
continue
- args = get_item_data(row, row.qty, valuation_rate)
+ args = get_item_data(row, row.qty, row.valuation_rate)
res.append(args)
else:
stock_bal = get_stock_balance(
@@ -1413,6 +1409,7 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
"item_code": row[0],
"warehouse": row[3],
"qty": row[8],
+ "valuation_rate": row[9],
"item_name": row[1],
"batch_no": row[4],
}
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index 026a064c7dc..559e1b31a36 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -282,7 +282,11 @@ class StockBalanceReport:
for field in self.inventory_dimensions:
qty_dict[field] = entry.get(field)
- if entry.voucher_type == "Stock Reconciliation" and (not entry.batch_no or entry.serial_no):
+ if (
+ entry.voucher_type == "Stock Reconciliation"
+ and frappe.get_cached_value(entry.voucher_type, entry.voucher_no, "purpose") != "Opening Stock"
+ and (not entry.batch_no or entry.serial_no)
+ ):
qty_diff = flt(entry.qty_after_transaction) - flt(qty_dict.bal_qty)
else:
qty_diff = flt(entry.actual_qty)
diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py
index e650fb607ba..b2401da4f8f 100644
--- a/erpnext/stock/stock_balance.py
+++ b/erpnext/stock/stock_balance.py
@@ -3,6 +3,7 @@
import frappe
+from frappe.query_builder.functions import Coalesce, Sum
from frappe.utils import cstr, flt, now, nowdate, nowtime
from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
@@ -182,18 +183,67 @@ def get_indented_qty(item_code, warehouse):
def get_ordered_qty(item_code, warehouse):
- ordered_qty = frappe.db.sql(
- """
- select sum((po_item.qty - po_item.received_qty)*po_item.conversion_factor)
- from `tabPurchase Order Item` po_item, `tabPurchase Order` po
- where po_item.item_code=%s and po_item.warehouse=%s
- and po_item.qty > po_item.received_qty and po_item.parent=po.name
- and po.status not in ('Closed', 'Delivered') and po.docstatus=1
- and po_item.delivered_by_supplier = 0""",
- (item_code, warehouse),
+ """Return total pending ordered quantity for an item in a warehouse.
+ Includes outstanding quantities from Purchase Orders and Subcontracting Orders"""
+
+ purchase_order_qty = get_purchase_order_qty(item_code, warehouse)
+ subcontracting_order_qty = get_subcontracting_order_qty(item_code, warehouse)
+
+ return flt(purchase_order_qty) + flt(subcontracting_order_qty)
+
+
+def get_purchase_order_qty(item_code, warehouse):
+ PurchaseOrder = frappe.qb.DocType("Purchase Order")
+ PurchaseOrderItem = frappe.qb.DocType("Purchase Order Item")
+
+ purchase_order_qty = (
+ frappe.qb.from_(PurchaseOrderItem)
+ .join(PurchaseOrder)
+ .on(PurchaseOrderItem.parent == PurchaseOrder.name)
+ .select(
+ Sum(
+ (PurchaseOrderItem.qty - PurchaseOrderItem.received_qty) * PurchaseOrderItem.conversion_factor
+ )
+ )
+ .where(
+ (PurchaseOrderItem.item_code == item_code)
+ & (PurchaseOrderItem.warehouse == warehouse)
+ & (PurchaseOrderItem.qty > PurchaseOrderItem.received_qty)
+ & (PurchaseOrder.status.notin(["Closed", "Delivered"]))
+ & (PurchaseOrder.docstatus == 1)
+ & (Coalesce(PurchaseOrderItem.delivered_by_supplier, 0) == 0)
+ )
+ .run()
)
- return flt(ordered_qty[0][0]) if ordered_qty else 0
+ return purchase_order_qty[0][0] if purchase_order_qty else 0
+
+
+def get_subcontracting_order_qty(item_code, warehouse):
+ SubcontractingOrder = frappe.qb.DocType("Subcontracting Order")
+ SubcontractingOrderItem = frappe.qb.DocType("Subcontracting Order Item")
+
+ subcontracting_order_qty = (
+ frappe.qb.from_(SubcontractingOrderItem)
+ .join(SubcontractingOrder)
+ .on(SubcontractingOrderItem.parent == SubcontractingOrder.name)
+ .select(
+ Sum(
+ (SubcontractingOrderItem.qty - SubcontractingOrderItem.received_qty)
+ * SubcontractingOrderItem.conversion_factor
+ )
+ )
+ .where(
+ (SubcontractingOrderItem.item_code == item_code)
+ & (SubcontractingOrderItem.warehouse == warehouse)
+ & (SubcontractingOrderItem.qty > SubcontractingOrderItem.received_qty)
+ & (SubcontractingOrder.status.notin(["Closed", "Completed"]))
+ & (SubcontractingOrder.docstatus == 1)
+ )
+ .run()
+ )
+
+ return subcontracting_order_qty[0][0] if subcontracting_order_qty else 0
def get_planned_qty(item_code, warehouse):
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
index ee9cf7a8ee5..8eb369d120f 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
@@ -12,7 +12,7 @@ from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry impor
StockReservation,
has_reserved_stock,
)
-from erpnext.stock.stock_balance import update_bin_qty
+from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
from erpnext.stock.utils import get_bin
@@ -234,30 +234,7 @@ class SubcontractingOrder(SubcontractingController):
):
item_wh_list.append([item.item_code, item.warehouse])
for item_code, warehouse in item_wh_list:
- update_bin_qty(item_code, warehouse, {"ordered_qty": self.get_ordered_qty(item_code, warehouse)})
-
- @staticmethod
- def get_ordered_qty(item_code, warehouse):
- table = frappe.qb.DocType("Subcontracting Order")
- child = frappe.qb.DocType("Subcontracting Order Item")
-
- query = (
- frappe.qb.from_(table)
- .inner_join(child)
- .on(table.name == child.parent)
- .select((child.qty - child.received_qty) * child.conversion_factor)
- .where(
- (table.docstatus == 1)
- & (child.item_code == item_code)
- & (child.warehouse == warehouse)
- & (child.qty > child.received_qty)
- & (table.status != "Completed")
- )
- )
-
- query = query.run()
-
- return flt(query[0][0]) if query else 0
+ update_bin_qty(item_code, warehouse, {"ordered_qty": get_ordered_qty(item_code, warehouse)})
def update_reserved_qty_for_subcontracting(self, sco_item_rows=None):
for item in self.supplied_items:
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
index 2a69083bc0f..29ecdd24b82 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
@@ -616,6 +616,117 @@ class TestSubcontractingReceipt(IntegrationTestCase):
for item in scr.supplied_items:
self.assertFalse(item.available_qty_for_consumption)
+ def test_supplied_items_consumed_qty_for_similar_finished_goods(self):
+ """
+ Test that supplied raw material consumption is calculated correctly
+ when multiple subcontracted service items use the same finished good
+ but different BOMs.
+ """
+
+ from erpnext.controllers.subcontracting_controller import (
+ make_rm_stock_entry as make_subcontract_transfer_entry,
+ )
+ from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+
+ # Configuration: Backflush based on subcontract material transfer
+ set_backflush_based_on("Material Transferred for Subcontract")
+
+ # Create Raw Materials
+ raw_material_1 = make_item("_RM Item 1", properties={"is_stock_item": 1}).name
+
+ raw_material_2 = make_item("_RM Item 2", properties={"is_stock_item": 1}).name
+
+ # Create Subcontracted Finished Good
+ finished_good = make_item("_Finished Good Item", properties={"is_stock_item": 1})
+ finished_good.is_sub_contracted_item = 1
+ finished_good.save()
+
+ # Receive Raw Materials into Warehouse
+ for raw_material in (raw_material_1, raw_material_2):
+ make_stock_entry(
+ item_code=raw_material,
+ qty=10,
+ target="_Test Warehouse - _TC",
+ basic_rate=100,
+ )
+
+ # Create BOMs for the same Finished Good with different RMs
+ bom_rm_1 = make_bom(
+ item=finished_good.name,
+ quantity=1,
+ raw_materials=[raw_material_1],
+ ).name
+
+ _bom_rm_2 = make_bom(
+ item=finished_good.name,
+ quantity=1,
+ raw_materials=[raw_material_2],
+ ).name
+
+ # Define Subcontracted Service Items
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 1,
+ "rate": 100,
+ "fg_item": finished_good.name,
+ "fg_item_qty": 10,
+ },
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 1,
+ "rate": 150,
+ "fg_item": finished_good.name,
+ "fg_item_qty": 10,
+ },
+ ]
+
+ # Create Subcontracting Order
+ subcontracting_order = get_subcontracting_order(
+ service_items=service_items,
+ do_not_save=True,
+ )
+
+ # Assign BOM only to the first service item
+ subcontracting_order.items[0].bom = bom_rm_1
+ subcontracting_order.save()
+ subcontracting_order.submit()
+
+ # Prepare Raw Material Transfer Items
+ raw_material_transfer_items = []
+ for supplied_item in subcontracting_order.supplied_items:
+ raw_material_transfer_items.append(
+ {
+ "item_code": supplied_item.main_item_code,
+ "rm_item_code": supplied_item.rm_item_code,
+ "qty": supplied_item.required_qty,
+ "warehouse": "_Test Warehouse - _TC",
+ "stock_uom": "Nos",
+ }
+ )
+
+ # Transfer Raw Materials to Subcontractor Warehouse
+ stock_entry = frappe.get_doc(
+ make_subcontract_transfer_entry(
+ subcontracting_order.name,
+ raw_material_transfer_items,
+ )
+ )
+ stock_entry.to_warehouse = "_Test Warehouse 1 - _TC"
+ stock_entry.save()
+ stock_entry.submit()
+
+ # Create Subcontracting Receipt
+ subcontracting_receipt = make_subcontracting_receipt(subcontracting_order.name)
+ subcontracting_receipt.save()
+
+ # Check consumed_qty for each supplied item
+ self.assertEqual(len(subcontracting_receipt.supplied_items), 2)
+ self.assertEqual(subcontracting_receipt.supplied_items[0].consumed_qty, 10)
+ self.assertEqual(subcontracting_receipt.supplied_items[1].consumed_qty, 10)
+
def test_supplied_items_cost_after_reposting(self):
# Set Backflush Based On as "BOM"
set_backflush_based_on("BOM")
diff --git a/erpnext/workspace_sidebar/invoicing.json b/erpnext/workspace_sidebar/invoicing.json
index 104c8547d5f..99a6b367953 100644
--- a/erpnext/workspace_sidebar/invoicing.json
+++ b/erpnext/workspace_sidebar/invoicing.json
@@ -219,7 +219,7 @@
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
- "label": "Payment Reconciliaition",
+ "label": "Payment Reconciliation",
"link_to": "Payment Reconciliation",
"link_type": "DocType",
"show_arrow": 0,
diff --git a/erpnext/workspace_sidebar/taxes.json b/erpnext/workspace_sidebar/taxes.json
index 5cf65ff3c67..64b343ca215 100644
--- a/erpnext/workspace_sidebar/taxes.json
+++ b/erpnext/workspace_sidebar/taxes.json
@@ -13,7 +13,7 @@
"indent": 0,
"keep_closed": 0,
"label": "Sales Tax Template",
- "link_to": "Item Tax Template",
+ "link_to": "Sales Taxes and Charges Template",
"link_type": "DocType",
"navigate_to_tab": "",
"show_arrow": 0,
@@ -148,7 +148,7 @@
"type": "Link"
}
],
- "modified": "2026-01-10 00:06:13.005238",
+ "modified": "2026-02-01 00:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Taxes",