diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js
index 77af313dc16..0a268aa88a2 100644
--- a/erpnext/accounts/doctype/bank/bank.js
+++ b/erpnext/accounts/doctype/bank/bank.js
@@ -3,9 +3,6 @@
frappe.provide("erpnext.integrations");
frappe.ui.form.on("Bank", {
- onload: function (frm) {
- add_fields_to_mapping_table(frm);
- },
refresh: function (frm) {
add_fields_to_mapping_table(frm);
frm.toggle_display(["address_html", "contact_html"], !frm.doc.__islocal);
@@ -37,11 +34,11 @@ let add_fields_to_mapping_table = function (frm) {
});
});
- frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property(
- "bank_transaction_field",
- "options",
- options
- );
+ const grid = frm.fields_dict.bank_transaction_mapping?.grid;
+
+ if (grid) {
+ grid.update_docfield_property("bank_transaction_field", "options", options);
+ }
};
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
@@ -116,7 +113,7 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
"There was an issue connecting to Plaid's authentication server. Check browser console for more information"
)
);
- console.log(error);
+ console.error(error);
}
plaid_success(token, response) {
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index e7c0832554f..2ed9881772c 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -172,7 +172,7 @@ class JournalEntry(AccountsController):
validate_docs_for_deferred_accounting([self.name], [])
def submit(self):
- if len(self.accounts) > 100:
+ if len(self.accounts) > 100 and not self.meta.queue_in_background:
queue_submission(self, "_submit")
else:
return self._submit()
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 852fcc6807b..580af69c404 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -400,6 +400,16 @@ frappe.ui.form.on("Payment Entry", {
);
frm.refresh_fields();
+
+ const party_currency =
+ frm.doc.payment_type === "Receive" ? "paid_from_account_currency" : "paid_to_account_currency";
+
+ var reference_grid = frm.fields_dict["references"].grid;
+ ["total_amount", "outstanding_amount", "allocated_amount"].forEach((fieldname) => {
+ reference_grid.update_docfield_property(fieldname, "options", party_currency);
+ });
+
+ reference_grid.refresh();
},
show_general_ledger: function (frm) {
@@ -1119,7 +1129,7 @@ frappe.ui.form.on("Payment Entry", {
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
await frm.call("allocate_amount_to_references", {
- paid_amount: paid_amount,
+ paid_amount: flt(paid_amount),
paid_amount_change: paid_amount_change,
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
});
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json
index 30a2da24bbb..80333239ad6 100644
--- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json
+++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json
@@ -132,6 +132,12 @@
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
+ },
+ {
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "label": "Project",
+ "options": "Project"
},
{
"fieldname": "due_date",
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 c7cc97d7197..bcf3ddec01a 100644
--- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py
+++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py
@@ -38,6 +38,7 @@ class PaymentLedgerEntry(Document):
amount_in_account_currency: DF.Currency
company: DF.Link | None
cost_center: DF.Link | None
+ project: DF.Link | None
delinked: DF.Check
due_date: DF.Date | None
finance_book: DF.Link | None
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index c81863f044f..9ec4e0a073a 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -746,7 +746,7 @@ class PaymentReconciliation(Document):
ple = qb.DocType("Payment Ledger Entry")
for x in self.dimensions:
dimension = x.fieldname
- if self.get(dimension):
+ if self.get(dimension) and frappe.db.has_column("Payment Ledger Entry", dimension):
self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension))
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 9711c4637bc..2c96286d3bb 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -117,12 +117,20 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
return item.delivery_note ? true : false;
});
- if (!from_delivery_note && !is_delivered_by_supplier) {
- cur_frm.add_custom_button(
- __("Delivery"),
- cur_frm.cscript["Make Delivery Note"],
- __("Create")
+ if (!is_delivered_by_supplier) {
+ const should_create_delivery_note = doc.items.some(
+ (item) =>
+ item.qty - item.delivered_qty > 0 &&
+ !item.dn_detail &&
+ !item.delivered_by_supplier
);
+ if (should_create_delivery_note) {
+ this.frm.add_custom_button(
+ __("Delivery Note"),
+ this.frm.cscript["Make Delivery Note"],
+ __("Create")
+ );
+ }
}
}
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 9e15f8701bb..8ed23724dfb 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -2211,7 +2211,9 @@ def make_delivery_note(source_name, target_doc=None):
"cost_center": "cost_center",
},
"postprocess": update_item,
- "condition": lambda doc: doc.delivered_by_supplier != 1,
+ "condition": lambda doc: doc.delivered_by_supplier != 1
+ and not doc.dn_detail
+ and doc.qty - doc.delivered_qty > 0,
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
"Sales Team": {
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 5a7c6ca9055..2a977dd2c03 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -1868,6 +1868,7 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
account=gle.account,
party_type=gle.party_type,
party=gle.party,
+ project=gle.project,
cost_center=gle.cost_center,
finance_book=gle.finance_book,
due_date=gle.due_date,
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index c9968c7a34d..7637192ba9b 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -669,7 +669,10 @@ class Asset(AccountsController):
def get_status(self):
"""Returns status based on whether it is draft, submitted, scrapped or depreciated"""
if self.docstatus == 0:
- status = "Draft"
+ if self.is_composite_asset:
+ status = "Work In Progress"
+ else:
+ status = "Draft"
elif self.docstatus == 1:
status = "Submitted"
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index 674cb3ffa3d..d234b162ba2 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -611,14 +611,21 @@ class AssetCapitalization(StockController):
asset_doc = frappe.get_doc("Asset", self.target_asset)
if self.docstatus == 2:
- asset_doc.gross_purchase_amount -= total_target_asset_value
- asset_doc.purchase_amount -= total_target_asset_value
+ gross_purchase_amount = asset_doc.gross_purchase_amount - total_target_asset_value
+ purchase_amount = asset_doc.purchase_amount - total_target_asset_value
+ total_asset_cost = asset_doc.total_asset_cost - total_target_asset_value
else:
- asset_doc.gross_purchase_amount += total_target_asset_value
- asset_doc.purchase_amount += total_target_asset_value
- asset_doc.set_status("Work In Progress")
- asset_doc.flags.ignore_validate = True
- asset_doc.save()
+ gross_purchase_amount = asset_doc.gross_purchase_amount + total_target_asset_value
+ purchase_amount = asset_doc.purchase_amount + total_target_asset_value
+ total_asset_cost = asset_doc.total_asset_cost + total_target_asset_value
+
+ asset_doc.db_set(
+ {
+ "gross_purchase_amount": gross_purchase_amount,
+ "purchase_amount": purchase_amount,
+ "total_asset_cost": total_asset_cost,
+ }
+ )
frappe.msgprint(
_("Asset {0} has been updated. Please set the depreciation details if any and submit it.").format(
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 035dfc6e264..bf91f9490a4 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -465,7 +465,7 @@ class StockController(AccountsController):
if is_rejected:
serial_nos = row.get("rejected_serial_no")
type_of_transaction = "Inward" if not self.is_return else "Outward"
- qty = row.get("rejected_qty")
+ qty = row.get("rejected_qty") * row.get("conversion_factor", 1.0)
warehouse = row.get("rejected_warehouse")
if (
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 313a9de4e4b..c48c56df3d5 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -553,8 +553,6 @@ var get_bom_material_detail = function (doc, cdt, cdn, scrap_items) {
do_not_explode: d.do_not_explode,
},
callback: function (r) {
- d = locals[cdt][cdn];
-
$.extend(d, r.message);
refresh_field("items");
refresh_field("scrap_items");
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 4640f5192dd..f1c9b706ca0 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -3186,6 +3186,53 @@ class TestWorkOrder(FrappeTestCase):
allow_overproduction("overproduction_percentage_for_work_order", 0)
+ def test_reserved_qty_for_pp_with_extra_material_transfer(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import (
+ make_stock_entry as make_stock_entry_test_record,
+ )
+
+ rm_item_code = make_item(
+ "_Test Reserved Qty PP Item",
+ {
+ "is_stock_item": 1,
+ },
+ ).name
+
+ fg_item_code = make_item(
+ "_Test Reserved Qty PP FG Item",
+ {
+ "is_stock_item": 1,
+ },
+ ).name
+
+ make_stock_entry_test_record(
+ item_code=rm_item_code, target="_Test Warehouse - _TC", qty=10, basic_rate=100
+ )
+
+ make_bom(
+ item=fg_item_code,
+ raw_materials=[rm_item_code],
+ )
+
+ wo_order = make_wo_order_test_record(
+ item=fg_item_code,
+ qty=1,
+ source_warehouse="_Test Warehouse - _TC",
+ skip_transfer=0,
+ target_warehouse="_Test Warehouse - _TC",
+ )
+
+ bin1_at_completion = get_bin(rm_item_code, "_Test Warehouse - _TC")
+ self.assertEqual(bin1_at_completion.reserved_qty_for_production, 1)
+
+ s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1))
+ s.items[0].qty += 2 # extra material transfer
+ s.submit()
+
+ bin1_at_completion = get_bin(rm_item_code, "_Test Warehouse - _TC")
+
+ self.assertEqual(bin1_at_completion.reserved_qty_for_production, 0)
+
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index c26af9a74af..f5a5e2693b9 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -517,6 +517,7 @@ class WorkOrder(Document):
self.db_set("status", "Cancelled")
self.on_close_or_cancel()
+ self.delete_job_card()
def on_close_or_cancel(self):
if self.production_plan and frappe.db.exists(
@@ -526,7 +527,6 @@ class WorkOrder(Document):
else:
self.update_work_order_qty_in_so()
- self.delete_job_card()
self.update_completed_qty_in_material_request()
self.update_planned_qty()
self.update_ordered_qty()
@@ -1786,6 +1786,9 @@ def get_reserved_qty_for_production(
qty_field = wo_item.required_qty
else:
qty_field = Case()
+ qty_field = qty_field.when(
+ ((wo.skip_transfer == 0) & (wo_item.transferred_qty > wo_item.required_qty)), 0.0
+ )
qty_field = qty_field.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty)
qty_field = qty_field.else_(wo_item.required_qty - wo_item.consumed_qty)
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index cc8f2434513..1f434a485b5 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -603,7 +603,7 @@ def send_project_update_email_to_users(project):
"sent": 0,
"date": today(),
"time": nowtime(),
- "naming_series": "UPDATE-.project.-.YY.MM.DD.-",
+ "naming_series": "UPDATE-.project.-.YY.MM.DD.-.####",
}
).insert()
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 0ae1828ee0d..2e782b49e66 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -2726,10 +2726,16 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
set_warehouse() {
this.autofill_warehouse(this.frm.doc.items, "warehouse", this.frm.doc.set_warehouse);
+ this.autofill_warehouse(this.frm.doc.packed_items, "warehouse", this.frm.doc.set_warehouse);
}
set_target_warehouse() {
this.autofill_warehouse(this.frm.doc.items, "target_warehouse", this.frm.doc.set_target_warehouse);
+ this.autofill_warehouse(
+ this.frm.doc.packed_items,
+ "target_warehouse",
+ this.frm.doc.set_target_warehouse
+ );
}
set_from_warehouse() {
diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js
index 7e2271dc38f..c9b5ed7e6f2 100644
--- a/erpnext/public/js/utils/sales_common.js
+++ b/erpnext/public/js/utils/sales_common.js
@@ -488,7 +488,30 @@ erpnext.sales_common = {
}
}
- project() {
+ project(doc, cdt, cdn) {
+ if (!cdt || !cdn) {
+ if (this.frm.doc.project) {
+ $.each(this.frm.doc["items"] || [], function (i, item) {
+ if (!item.project) {
+ frappe.model.set_value(item.doctype, item.name, "project", doc.project);
+ }
+ });
+ }
+ } else {
+ const item = frappe.get_doc(cdt, cdn);
+ if (item.project) {
+ $.each(this.frm.doc["items"] || [], function (i, other_item) {
+ if (!other_item.project) {
+ frappe.model.set_value(
+ other_item.doctype,
+ other_item.name,
+ "project",
+ item.project
+ );
+ }
+ });
+ }
+ }
let me = this;
if (["Delivery Note", "Sales Invoice", "Sales Order"].includes(this.frm.doc.doctype)) {
if (this.frm.doc.project) {
diff --git a/erpnext/regional/address_template/templates/sweden.html b/erpnext/regional/address_template/templates/sweden.html
new file mode 100644
index 00000000000..0c2ed73f0ae
--- /dev/null
+++ b/erpnext/regional/address_template/templates/sweden.html
@@ -0,0 +1,4 @@
+{{ address_line1 }}
+{% if address_line2 %}{{ address_line2 }}
{% endif -%}
+{{ pincode }} {{ city | upper }}
+{{ country | upper }}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index 7c0676c672f..dd1910ed15a 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -181,6 +181,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Customer Group",
+ "link_filters": "[[\"Customer Group\", \"is_group\", \"=\", 0]]",
"oldfieldname": "customer_group",
"oldfieldtype": "Link",
"options": "Customer Group",
@@ -610,7 +611,7 @@
"link_fieldname": "party"
}
],
- "modified": "2025-11-25 09:35:56.772949",
+ "modified": "2026-01-21 17:23:42.151114",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 1c1ae08b280..7fb15943c9f 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -114,6 +114,7 @@ class Customer(TransactionBase):
set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
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
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index 83ba1907671..d2f8945eb29 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -51,6 +51,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Default Customer Group",
+ "link_filters": "[[\"Customer Group\", \"is_group\", \"=\", 0]]",
"options": "Customer Group"
},
{
@@ -231,7 +232,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2025-09-23 21:10:14.826653",
+ "modified": "2026-01-21 17:28:37.027837",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",
diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json
index e82daeb7c72..3616425771c 100644
--- a/erpnext/setup/setup_wizard/data/country_wise_tax.json
+++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json
@@ -6,14 +6,14 @@
}
},
"Algeria": {
- "Algeria VAT 17%": {
- "account_name": "VAT 17%",
- "tax_rate": 17.00,
+ "Algeria TVA 19%": {
+ "account_name": "TVA 19%",
+ "tax_rate": 19.00,
"default": 1
},
- "Algeria VAT 7%": {
- "account_name": "VAT 7%",
- "tax_rate": 7.00
+ "Algeria TVA 9%": {
+ "account_name": "TVA 9%",
+ "tax_rate": 9.00
}
},
diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py
index f6984c9edab..69443e3a608 100644
--- a/erpnext/stock/deprecated_serial_batch.py
+++ b/erpnext/stock/deprecated_serial_batch.py
@@ -78,7 +78,6 @@ class DeprecatedBatchNoValuation:
for ledger in entries:
self.stock_value_differece[ledger.batch_no] += flt(ledger.batch_value)
self.available_qty[ledger.batch_no] += flt(ledger.batch_qty)
- self.total_qty[ledger.batch_no] += flt(ledger.batch_qty)
@deprecated
def get_sle_for_batches(self):
@@ -231,7 +230,6 @@ class DeprecatedBatchNoValuation:
batch_data = query.run(as_dict=True)
for d in batch_data:
self.available_qty[d.batch_no] += flt(d.batch_qty)
- self.total_qty[d.batch_no] += flt(d.batch_qty)
for d in batch_data:
if self.available_qty.get(d.batch_no):
@@ -332,7 +330,6 @@ class DeprecatedBatchNoValuation:
batch_data = query.run(as_dict=True)
for d in batch_data:
self.available_qty[d.batch_no] += flt(d.batch_qty)
- self.total_qty[d.batch_no] += flt(d.batch_qty)
if not self.last_sle:
return
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index fdaaa23c5af..2f98aff00f2 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -228,8 +228,25 @@ class Item(Document):
def validate_description(self):
"""Clean HTML description if set"""
if cint(frappe.db.get_single_value("Stock Settings", "clean_description_html")):
+ old_desc = self.description
self.description = clean_html(self.description)
+ if (
+ old_desc
+ and self.description
+ and "
{
- if (!(r.last_name && r.email && (r.phone || r.mobile_no))) {
+ if (!(r.full_name && r.email && (r.phone || r.mobile_no))) {
if (delivery_type == "Delivery") {
frm.set_value("delivery_company", "");
frm.set_value("delivery_contact", "");
@@ -272,9 +272,9 @@ frappe.ui.form.on("Shipment", {
frm.set_value("pickup_contact", "");
}
frappe.throw(
- __("Last Name, Email or Phone/Mobile of the user are mandatory to continue.") +
+ __("Full Name, Email or Phone/Mobile of the user are mandatory to continue.") +
"" +
- __("Please first set Last Name, Email and Phone for the user") +
+ __("Please first set Full Name, Email and Phone for the user") +
` ${frappe.session.user}`
);
}
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 21f3245bbc4..d313c037e02 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -243,6 +243,27 @@ class StockEntry(StockController):
self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
self.validate_same_source_target_warehouse_during_material_transfer()
+ self.validate_raw_materials_exists()
+
+ def validate_raw_materials_exists(self):
+ if self.purpose not in ["Manufacture", "Repack", "Disassemble"]:
+ return
+
+ if frappe.db.get_single_value("Manufacturing Settings", "material_consumption"):
+ return
+
+ raw_materials = []
+ for row in self.items:
+ if row.s_warehouse:
+ raw_materials.append(row.item_code)
+
+ if not raw_materials:
+ frappe.throw(
+ _(
+ "At least one raw material item must be present in the stock entry for the type {0}"
+ ).format(bold(self.purpose)),
+ title=_("Raw Materials Missing"),
+ )
def set_serial_batch_for_disassembly(self):
if self.purpose != "Disassemble":
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 0be02756207..2383fabaf89 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -2232,6 +2232,28 @@ class TestStockEntry(FrappeTestCase):
se.save()
se.submit()
+ def test_raw_material_missing_validation(self):
+ original_value = frappe.db.get_single_value("Manufacturing Settings", "material_consumption")
+ frappe.db.set_single_value("Manufacturing Settings", "material_consumption", 0)
+
+ stock_entry = make_stock_entry(
+ item_code="_Test Item",
+ qty=1,
+ target="_Test Warehouse - _TC",
+ do_not_save=True,
+ )
+
+ stock_entry.purpose = "Manufacture"
+ stock_entry.stock_entry_type = "Manufacture"
+ stock_entry.items[0].is_finished_item = 1
+
+ self.assertRaises(
+ frappe.ValidationError,
+ stock_entry.save,
+ )
+
+ frappe.db.set_single_value("Manufacturing Settings", "material_consumption", original_value)
+
def make_serialized_item(**args):
args = frappe._dict(args)
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index b0ade4324fa..312b8e129f8 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -421,9 +421,10 @@ def get_basic_details(args, item, overwrite_warehouse=True):
if not args.get("uom"):
if args.get("doctype") in sales_doctypes:
args.uom = item.sales_uom if item.sales_uom else item.stock_uom
- elif (args.get("doctype") in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]) or (
- args.get("doctype") == "Material Request" and args.get("material_request_type") == "Purchase"
- ):
+ elif (
+ args.get("doctype")
+ in ["Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation"]
+ ) or (args.get("doctype") == "Material Request" and args.get("material_request_type") == "Purchase"):
args.uom = item.purchase_uom if item.purchase_uom else item.stock_uom
else:
args.uom = item.stock_uom
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index f2639998d3e..4d7b3d4aaa4 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -804,52 +804,10 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate)
self.available_qty[ledger.batch_no] += flt(ledger.qty)
- entries = self.get_batch_wise_total_available_qty()
- for row in entries:
- self.total_qty[row.batch_no] += flt(row.total_qty)
-
self.calculate_avg_rate_from_deprecarated_ledgers()
self.calculate_avg_rate_for_non_batchwise_valuation()
self.set_stock_value_difference()
- def get_batch_wise_total_available_qty(self) -> list[dict]:
- # Get total qty of each batch no from Serial and Batch Bundle without checking time condition
- if not self.batchwise_valuation_batches:
- return []
-
- parent = frappe.qb.DocType("Serial and Batch Bundle")
- child = frappe.qb.DocType("Serial and Batch Entry")
-
- query = (
- frappe.qb.from_(parent)
- .inner_join(child)
- .on(parent.name == child.parent)
- .select(
- child.batch_no,
- Sum(child.qty).as_("total_qty"),
- )
- .where(
- (parent.warehouse == self.sle.warehouse)
- & (parent.item_code == self.sle.item_code)
- & (child.batch_no.isin(self.batchwise_valuation_batches))
- & (parent.docstatus == 1)
- & (parent.is_cancelled == 0)
- & (parent.type_of_transaction.isin(["Inward", "Outward"]))
- )
- .for_update()
- .groupby(child.batch_no)
- )
-
- # Important to exclude the current voucher detail no / voucher no to calculate the correct stock value difference
- if self.sle.voucher_detail_no:
- query = query.where(parent.voucher_detail_no != self.sle.voucher_detail_no)
- elif self.sle.voucher_no:
- query = query.where(parent.voucher_no != self.sle.voucher_no)
-
- query = query.where(parent.voucher_type != "Pick List")
-
- return query.run(as_dict=True)
-
def get_batch_no_ledgers(self) -> list[dict]:
# Get batch wise stock value difference from Serial and Batch Bundle considering time condition
if not self.batchwise_valuation_batches: