Merge pull request #52104 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2026-01-28 09:43:01 +05:30
committed by GitHub
33 changed files with 380 additions and 105 deletions

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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,
});

View File

@@ -132,6 +132,12 @@
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "due_date",

View File

@@ -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

View File

@@ -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):

View File

@@ -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")
);
}
}
}

View File

@@ -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": {

View File

@@ -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,

View File

@@ -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"

View File

@@ -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(

View File

@@ -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 (

View File

@@ -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");

View File

@@ -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 (

View File

@@ -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)

View File

@@ -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()

View File

@@ -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() {

View File

@@ -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) {

View File

@@ -0,0 +1,4 @@
{{ address_line1 }}<br>
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
{{ pincode }} {{ city | upper }}<br>
{{ country | upper }}

View File

@@ -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",

View File

@@ -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

View File

@@ -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",

View File

@@ -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
}
},

View File

@@ -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

View File

@@ -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 "<img src" in old_desc
and "<img src" not in self.description
):
frappe.msgprint(
_(
'Image in the description has been removed. To disable this behavior, uncheck "{0}" in {1}.'
).format(
frappe.get_meta("Stock Settings").get_label("clean_description_html"),
get_link_to_form("Stock Settings"),
),
alert=True,
)
def validate_customer_provided_part(self):
if self.is_customer_provided_item:
if self.is_purchase_item:

View File

@@ -281,7 +281,6 @@
{
"fieldname": "set_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Set Target Warehouse",
"options": "Warehouse"
@@ -368,7 +367,7 @@
"idx": 70,
"is_submittable": 1,
"links": [],
"modified": "2025-07-31 17:19:01.166208",
"modified": "2026-01-21 12:48:40.792323",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",

View File

@@ -4677,6 +4677,45 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(frappe.parse_json(stock_queue), [[20, 0.0]])
def test_negative_stock_error_for_purchase_return(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 Negative Stock for Purchase Return Item",
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TNSFPRI.#####"},
).name
pr = make_purchase_receipt(
item_code=item_code,
posting_date=add_days(today(), -3),
qty=10,
rate=100,
warehouse="_Test Warehouse - _TC",
)
batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
make_purchase_receipt(
item_code=item_code,
posting_date=add_days(today(), -4),
qty=10,
rate=100,
warehouse="_Test Warehouse - _TC",
)
make_stock_entry(
item_code=item_code,
qty=10,
source="_Test Warehouse - _TC",
target="_Test Warehouse 1 - _TC",
batch_no=batch_no,
use_serial_batch_fields=1,
)
return_pr = make_return_doc("Purchase Receipt", pr.name)
self.assertRaises(frappe.ValidationError, return_pr.submit)
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -576,14 +576,12 @@ class SerialandBatchBundle(Document):
d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no)))
precision = d.precision("qty")
for field in ["available_qty", "total_qty"]:
value = getattr(sn_obj, field)
available_qty = flt(value.get(d.batch_no), precision)
if self.docstatus == 1:
available_qty += flt(d.qty, precision)
available_qty = flt(sn_obj.available_qty.get(d.batch_no), precision)
if self.docstatus == 1:
available_qty += flt(d.qty, precision)
if not allow_negative_stock:
self.validate_negative_batch(d.batch_no, available_qty, field)
if not allow_negative_stock:
self.validate_negative_batch(d.batch_no, available_qty)
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
@@ -596,8 +594,8 @@ class SerialandBatchBundle(Document):
}
)
def validate_negative_batch(self, batch_no, available_qty, field=None):
if available_qty < 0 and not self.is_stock_reco_for_valuation_adjustment(available_qty, field=field):
def validate_negative_batch(self, batch_no, available_qty):
if available_qty < 0 and not self.is_stock_reco_for_valuation_adjustment(available_qty):
msg = f"""Batch No {bold(batch_no)} of an Item {bold(self.item_code)}
has negative stock
of quantity {bold(available_qty)} in the
@@ -605,7 +603,7 @@ class SerialandBatchBundle(Document):
frappe.throw(_(msg), BatchNegativeStockError)
def is_stock_reco_for_valuation_adjustment(self, available_qty, field=None):
def is_stock_reco_for_valuation_adjustment(self, available_qty):
if (
self.voucher_type == "Stock Reconciliation"
and self.type_of_transaction == "Outward"
@@ -613,7 +611,6 @@ class SerialandBatchBundle(Document):
and (
abs(frappe.db.get_value("Stock Reconciliation Item", self.voucher_detail_no, "qty"))
== abs(available_qty)
or field == "total_qty"
)
):
return True
@@ -1346,6 +1343,7 @@ class SerialandBatchBundle(Document):
def on_submit(self):
self.validate_serial_nos_inventory()
self.validate_batch_quantity()
def set_purchase_document_no(self):
if self.flags.ignore_validate_serial_batch:
@@ -1404,6 +1402,110 @@ class SerialandBatchBundle(Document):
def on_cancel(self):
self.validate_voucher_no_docstatus()
self.validate_batch_quantity()
def validate_batch_quantity(self):
if not self.has_batch_no:
return
if self.type_of_transaction != "Outward" or (
self.voucher_type == "Stock Reconciliation" and self.type_of_transaction == "Outward"
):
return
batch_wise_available_qty = self.get_batchwise_available_qty()
precision = frappe.get_precision("Serial and Batch Entry", "qty")
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"),
)
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()
if not available_qty_from_ledger:
return available_qty
for batch_no, qty in available_qty_from_ledger.items():
if batch_no in available_qty:
available_qty[batch_no] += qty
else:
available_qty[batch_no] = qty
return available_qty
def get_available_qty_from_stock_ledger(self):
batches = [d.batch_no for d in self.entries if d.batch_no]
sle = frappe.qb.DocType("Stock Ledger Entry")
query = (
frappe.qb.from_(sle)
.select(
sle.batch_no,
Sum(sle.actual_qty).as_("available_qty"),
)
.where(
(sle.item_code == self.item_code)
& (sle.warehouse == self.warehouse)
& (sle.is_cancelled == 0)
& (sle.batch_no.isin(batches))
& (sle.docstatus == 1)
& (sle.serial_and_batch_bundle.isnull())
& (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()
def get_available_qty_from_sabb(self):
batches = [d.batch_no for d in self.entries if d.batch_no]
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.warehouse)
& (parent.item_code == self.item_code)
& (child.batch_no.isin(batches))
& (parent.docstatus == 1)
& (parent.is_cancelled == 0)
& (parent.type_of_transaction.isin(["Inward", "Outward"]))
)
.for_update()
.groupby(child.batch_no)
)
query = query.where(parent.voucher_type != "Pick List")
res = query.run(as_list=True)
return frappe._dict(res) if res else frappe._dict()
def validate_voucher_no_docstatus(self):
if self.voucher_type == "POS Invoice":

View File

@@ -261,9 +261,9 @@ frappe.ui.form.on("Shipment", {
frappe.db.get_value(
"User",
{ name: frappe.session.user },
["full_name", "last_name", "email", "phone", "mobile_no"],
["full_name", "email", "phone", "mobile_no"],
(r) => {
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.") +
"</br>" +
__("Please first set Last Name, Email and Phone for the user") +
__("Please first set Full Name, Email and Phone for the user") +
` <a href="/app/user/${frappe.session.user}">${frappe.session.user}</a>`
);
}

View File

@@ -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":

View File

@@ -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)

View File

@@ -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

View File

@@ -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: