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

chore: release v15
This commit is contained in:
ruthra kumar
2026-03-10 20:16:23 +05:30
committed by GitHub
26 changed files with 587 additions and 165 deletions

View File

@@ -728,9 +728,10 @@ class PurchaseInvoice(BuyingController):
for item in self.get("items"): for item in self.get("items"):
if item.purchase_receipt: if item.purchase_receipt:
frappe.throw( frappe.throw(
_("Stock cannot be updated against Purchase Receipt {0}").format( _(
item.purchase_receipt "Stock cannot be updated for Purchase Invoice {0} because a Purchase Receipt {1} has already been created for this transaction. Please disable the 'Update Stock' checkbox in the Purchase Invoice and save the invoice."
) ).format(self.name, item.purchase_receipt),
title=_("Stock Update Not Allowed"),
) )
def validate_for_repost(self): def validate_for_repost(self):

View File

@@ -1199,6 +1199,9 @@ class SalesInvoice(SellingController):
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note)) throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
def process_asset_depreciation(self): def process_asset_depreciation(self):
if self.is_internal_transfer():
return
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1): if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
self.depreciate_asset_on_sale() self.depreciate_asset_on_sale()
else: else:

View File

@@ -37,6 +37,20 @@ function get_filters() {
}); });
}, },
}, },
{
fieldname: "party_type",
label: __("Party Type"),
fieldtype: "Link",
options: "Party Type",
width: 100,
},
{
fieldname: "party",
label: __("Party"),
fieldtype: "Dynamic Link",
options: "party_type",
width: 100,
},
{ {
fieldname: "voucher_no", fieldname: "voucher_no",
label: __("Voucher No"), label: __("Voucher No"),

View File

@@ -68,6 +68,12 @@ class General_Payment_Ledger_Comparison:
if self.filters.period_end_date: if self.filters.period_end_date:
filter_criterion.append(gle.posting_date.lte(self.filters.period_end_date)) filter_criterion.append(gle.posting_date.lte(self.filters.period_end_date))
if self.filters.party_type:
filter_criterion.append(gle.party_type.eq(self.filters.party_type))
if self.filters.party:
filter_criterion.append(gle.party.eq(self.filters.party))
if acc_type == "receivable": if acc_type == "receivable":
outstanding = (Sum(gle.debit) - Sum(gle.credit)).as_("outstanding") outstanding = (Sum(gle.debit) - Sum(gle.credit)).as_("outstanding")
else: else:
@@ -111,6 +117,12 @@ class General_Payment_Ledger_Comparison:
if self.filters.period_end_date: if self.filters.period_end_date:
filter_criterion.append(ple.posting_date.lte(self.filters.period_end_date)) filter_criterion.append(ple.posting_date.lte(self.filters.period_end_date))
if self.filters.party_type:
filter_criterion.append(ple.party_type.eq(self.filters.party_type))
if self.filters.party:
filter_criterion.append(ple.party.eq(self.filters.party))
self.account_types[acc_type].ple = ( self.account_types[acc_type].ple = (
qb.from_(ple) qb.from_(ple)
.select( .select(

View File

@@ -649,7 +649,7 @@ class GrossProfitGenerator:
new_row = row new_row = row
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion) self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion)
else: else:
new_row.qty += flt(row.qty) new_row.qty = flt((new_row.qty + row.qty), self.float_precision)
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion, True) self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion, True)
new_row = self.set_average_rate(new_row) new_row = self.set_average_rate(new_row)
@@ -659,11 +659,17 @@ class GrossProfitGenerator:
if i == 0: if i == 0:
new_row = row new_row = row
else: else:
new_row.qty += flt(row.qty) new_row.qty = flt((new_row.qty + row.qty), self.float_precision)
new_row.buying_amount += flt(row.buying_amount, self.currency_precision) new_row.buying_amount = flt(
new_row.base_amount += flt(row.base_amount, self.currency_precision) (new_row.buying_amount + row.buying_amount), self.currency_precision
)
new_row.base_amount = flt(
(new_row.base_amount + row.base_amount), self.currency_precision
)
if self.filters.get("group_by") == "Sales Person": if self.filters.get("group_by") == "Sales Person":
new_row.allocated_amount += flt(row.allocated_amount, self.currency_precision) new_row.allocated_amount = flt(
(new_row.allocated_amount + row.allocated_amount), self.currency_precision
)
new_row = self.set_average_rate(new_row) new_row = self.set_average_rate(new_row)
self.grouped_data.append(new_row) self.grouped_data.append(new_row)

View File

@@ -111,16 +111,12 @@ frappe.ui.form.on("Asset Repair", {
purchase_invoice: function (frm) { purchase_invoice: function (frm) {
if (frm.doc.purchase_invoice) { if (frm.doc.purchase_invoice) {
frappe.call({ frappe.call({
method: "frappe.client.get_value", method: "erpnext.assets.doctype.asset_repair.asset_repair.get_repair_cost_for_purchase_invoice",
args: { args: {
doctype: "Purchase Invoice", purchase_invoice: frm.doc.purchase_invoice,
fieldname: "base_net_total",
filters: { name: frm.doc.purchase_invoice },
}, },
callback: function (r) { callback: function (r) {
if (r.message) { frm.set_value("repair_cost", r.message || 0);
frm.set_value("repair_cost", r.message.base_net_total);
}
}, },
}); });
} else { } else {

View File

@@ -3,6 +3,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import Sum
from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours
import erpnext import erpnext
@@ -308,9 +309,14 @@ class AssetRepair(AccountsController):
if flt(self.repair_cost) <= 0: if flt(self.repair_cost) <= 0:
return return
pi_expense_account = ( expense_accounts = _get_expense_accounts_for_purchase_invoice(self.purchase_invoice)
frappe.get_doc("Purchase Invoice", self.purchase_invoice).items[0].expense_account
) if not expense_accounts:
frappe.throw(
_("No expense accounts found for Purchase Invoice {0}").format(self.purchase_invoice)
)
pi_expense_account = expense_accounts[0]
gl_entries.append( gl_entries.append(
self.get_gl_dict( self.get_gl_dict(
@@ -473,3 +479,84 @@ class AssetRepair(AccountsController):
def get_downtime(failure_date, completion_date): def get_downtime(failure_date, completion_date):
downtime = time_diff_in_hours(completion_date, failure_date) downtime = time_diff_in_hours(completion_date, failure_date)
return round(downtime, 2) return round(downtime, 2)
@frappe.whitelist()
def get_repair_cost_for_purchase_invoice(purchase_invoice: str) -> float:
"""
Get the total repair cost from GL entries for a purchase invoice.
Only considers expense accounts for non-stock, non-fixed-asset items.
"""
if not purchase_invoice:
return 0.0
frappe.has_permission("Purchase Invoice", "read", purchase_invoice, throw=True)
expense_accounts = _get_expense_accounts_for_purchase_invoice(purchase_invoice)
if not expense_accounts:
return 0.0
return _get_total_expense_amount(purchase_invoice, expense_accounts)
def _get_expense_accounts_for_purchase_invoice(purchase_invoice: str) -> list[str]:
"""
Get expense accounts for non-stock items from the purchase invoice.
"""
pi_items = frappe.get_all(
"Purchase Invoice Item",
filters={"parent": purchase_invoice},
fields=["item_code", "expense_account", "is_fixed_asset"],
)
if not pi_items:
return []
# Get list of stock item codes from the invoice
item_codes = {item.item_code for item in pi_items if item.item_code}
stock_items = set()
if item_codes:
stock_items = set(
frappe.db.get_all(
"Item", filters={"name": ["in", list(item_codes)], "is_stock_item": 1}, pluck="name"
)
)
expense_accounts = set()
for item in pi_items:
# Skip stock items - they use warehouse accounts
if item.item_code and item.item_code in stock_items:
continue
# Skip fixed assets - they use asset accounts
if item.is_fixed_asset:
continue
# Use expense account from Purchase Invoice Item
if item.expense_account:
expense_accounts.add(item.expense_account)
return list(expense_accounts)
def _get_total_expense_amount(purchase_invoice: str, expense_accounts: list[str]) -> float:
"""Get the total expense amount from GL entries for a purchase invoice and accounts."""
if not expense_accounts:
return 0.0
gl_entry = frappe.qb.DocType("GL Entry")
result = (
frappe.qb.from_(gl_entry)
.select((Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("total"))
.where(
(gl_entry.voucher_type == "Purchase Invoice")
& (gl_entry.voucher_no == purchase_invoice)
& (gl_entry.account.isin(expense_accounts))
& (gl_entry.is_cancelled == 0)
)
).run(as_dict=True)
return flt(result[0].total) if result else 0.0

View File

@@ -8,6 +8,7 @@ from frappe import qb
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.assets.doctype.asset.asset import ( from erpnext.assets.doctype.asset.asset import (
get_asset_account, get_asset_account,
get_asset_value_after_depreciation, get_asset_value_after_depreciation,
@@ -21,6 +22,7 @@ from erpnext.assets.doctype.asset.test_asset import (
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc, get_asset_depr_schedule_doc,
) )
from erpnext.assets.doctype.asset_repair.asset_repair import get_repair_cost_for_purchase_invoice
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_serial_nos_from_bundle, get_serial_nos_from_bundle,
@@ -321,6 +323,59 @@ class TestAssetRepair(unittest.TestCase):
self.assertEqual(asset.additional_asset_cost, asset_repair.repair_cost) self.assertEqual(asset.additional_asset_cost, asset_repair.repair_cost)
self.assertEqual(booked_value, asset_repair.repair_cost) self.assertEqual(booked_value, asset_repair.repair_cost)
def test_repair_cost_fetches_only_service_item_amount(self):
"""Test that repair cost only includes service (non-stock) item amounts from purchase invoice."""
company = "_Test Company with perpetual inventory"
warehouse = "Stores - TCP1"
service_item = create_item(
"_Test Service Item for Repair",
is_stock_item=0,
warehouse=warehouse,
company=company,
)
stock_item = create_item(
"_Test Stock Item for Repair",
is_stock_item=1,
warehouse=warehouse,
company=company,
)
service_expense_account = "Miscellaneous Expenses - TCP1"
cost_center = frappe.db.get_value("Company", company, "cost_center")
pi = make_purchase_invoice(
item_code=service_item.name,
qty=1,
rate=500,
expense_account=service_expense_account,
cost_center=cost_center,
warehouse=warehouse,
update_stock=0,
do_not_submit=1,
company=company,
)
pi.update_stock = 1
pi.append(
"items",
{
"item_code": stock_item.name,
"qty": 2,
"rate": 300,
"warehouse": warehouse,
"cost_center": cost_center,
},
)
pi.save()
pi.submit()
repair_cost = get_repair_cost_for_purchase_invoice(pi.name)
self.assertEqual(repair_cost, 500)
def num_of_depreciations(asset): def num_of_depreciations(asset):
return asset.finance_books[0].total_number_of_depreciations return asset.finance_books[0].total_number_of_depreciations
@@ -411,6 +466,7 @@ def create_asset_repair(**args):
if asset.calculate_depreciation: if asset.calculate_depreciation:
asset_repair.increase_in_asset_life = 12 asset_repair.increase_in_asset_life = 12
pi = make_purchase_invoice( pi = make_purchase_invoice(
item=args.item or "_Test Non Stock Item",
company=asset.company, company=asset.company,
expense_account=frappe.db.get_value("Company", asset.company, "default_expense_account"), expense_account=frappe.db.get_value("Company", asset.company, "default_expense_account"),
cost_center=asset_repair.cost_center, cost_center=asset_repair.cost_center,

View File

@@ -37,7 +37,7 @@ class TestPurchaseOrder(FrappeTestCase):
self.assertEqual(sq.get("items")[0].qty, 5) self.assertEqual(sq.get("items")[0].qty, 5)
self.assertEqual(sq.get("items")[1].rate, 300) self.assertEqual(sq.get("items")[1].rate, 300)
def test_update_supplier_quotation_child_rate_disallow(self): def test_update_supplier_quotation_child_rate(self):
sq = frappe.copy_doc(test_records[0]) sq = frappe.copy_doc(test_records[0])
sq.submit() sq.submit()
trans_item = json.dumps( trans_item = json.dumps(
@@ -50,6 +50,22 @@ class TestPurchaseOrder(FrappeTestCase):
}, },
] ]
) )
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
sq.reload()
self.assertEqual(sq.get("items")[0].rate, 300)
po = make_purchase_order(sq.name)
po.schedule_date = add_days(today(), 1)
po.submit()
trans_item = json.dumps(
[
{
"item_code": sq.items[0].item_code,
"rate": 20,
"qty": sq.items[0].qty,
"docname": sq.items[0].name,
},
]
)
self.assertRaises( self.assertRaises(
frappe.ValidationError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name frappe.ValidationError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
) )

View File

@@ -3838,20 +3838,28 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False
return False return False
def validate_quantity(child_item, new_data): def validate_quantity_and_rate(child_item, new_data):
if not flt(new_data.get("qty")) and not is_allowed_zero_qty(): if not flt(new_data.get("qty")) and not is_allowed_zero_qty():
frappe.throw( frappe.throw(
_("Row #{0}: Quantity for Item {1} cannot be zero.").format( _("Row #{0}:Quantity for Item {1} cannot be zero.").format(
new_data.get("idx"), frappe.bold(new_data.get("item_code")) new_data.get("idx"), frappe.bold(new_data.get("item_code"))
), ),
title=_("Invalid Qty"), title=_("Invalid Qty"),
) )
if parent_doctype == "Sales Order" and flt(new_data.get("qty")) < flt(child_item.delivered_qty): qty_limits = {
frappe.throw(_("Cannot set quantity less than delivered quantity")) "Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity")),
"Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity")),
}
if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(child_item.received_qty): if parent_doctype in qty_limits:
frappe.throw(_("Cannot set quantity less than received quantity")) qty_field, error_message = qty_limits[parent_doctype]
if flt(new_data.get("qty")) < flt(child_item.get(qty_field)):
frappe.throw(
_("Row #{0}:").format(new_data.get("idx"))
+ error_message.format(frappe.bold(new_data.get("item_code"))),
title=_("Invalid Qty"),
)
if parent_doctype in ["Quotation", "Supplier Quotation"]: if parent_doctype in ["Quotation", "Supplier Quotation"]:
if (parent_doctype == "Quotation" and not ordered_items) or ( if (parent_doctype == "Quotation" and not ordered_items) or (
@@ -3864,7 +3872,15 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
if parent_doctype == "Quotation" if parent_doctype == "Quotation"
else purchased_items.get(child_item.name) else purchased_items.get(child_item.name)
) )
if qty_to_check: if qty_to_check:
if not rate_unchanged:
frappe.throw(
_(
"Cannot update rate as item {0} is already ordered or purchased against this quotation"
).format(frappe.bold(new_data.get("item_code")))
)
if flt(new_data.get("qty")) < qty_to_check: if flt(new_data.get("qty")) < qty_to_check:
frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity")) frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity"))
@@ -3980,10 +3996,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
): ):
continue continue
validate_quantity(child_item, d) validate_quantity_and_rate(child_item, d)
if parent_doctype in ["Quotation", "Supplier Quotation"]:
if not rate_unchanged:
frappe.throw(_("Rates cannot be modified for quoted items"))
if flt(child_item.get("qty")) != flt(d.get("qty")): if flt(child_item.get("qty")) != flt(d.get("qty")):
any_qty_changed = True any_qty_changed = True

View File

@@ -1135,6 +1135,16 @@ class StockController(AccountsController):
continue continue
if qi_required: # validate row only if inspection is required on item level if qi_required: # validate row only if inspection is required on item level
if self.doctype in [
"Purchase Receipt",
"Purchase Invoice",
"Sales Invoice",
"Delivery Note",
] and frappe.get_single_value(
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
):
return
self.validate_qi_presence(row) self.validate_qi_presence(row)
if self.docstatus == 1: if self.docstatus == 1:
self.validate_qi_submission(row) self.validate_qi_submission(row)
@@ -1142,16 +1152,6 @@ class StockController(AccountsController):
def validate_qi_presence(self, row): def validate_qi_presence(self, row):
"""Check if QI is present on row level. Warn on save and stop on submit if missing.""" """Check if QI is present on row level. Warn on save and stop on submit if missing."""
if self.doctype in [
"Purchase Receipt",
"Purchase Invoice",
"Sales Invoice",
"Delivery Note",
] and frappe.db.get_single_value(
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
):
return
if not row.quality_inspection: if not row.quality_inspection:
msg = _("Row #{0}: Quality Inspection is required for Item {1}").format( msg = _("Row #{0}: Quality Inspection is required for Item {1}").format(
row.idx, frappe.bold(row.item_code) row.idx, frappe.bold(row.item_code)

View File

@@ -1297,6 +1297,55 @@ def make_rm_stock_entry(
if target_doc and target_doc.get("items"): if target_doc and target_doc.get("items"):
target_doc.items = [] target_doc.items = []
def post_process(source_doc, target_doc):
target_doc.purpose = "Send to Subcontractor"
if order_doctype == "Purchase Order":
target_doc.purchase_order = source_doc.name
else:
target_doc.subcontracting_order = source_doc.name
target_doc.set_stock_entry_type()
for fg_item_code in fg_item_code_list:
for rm_item in rm_items:
if (
rm_item.get("main_item_code") == fg_item_code
or rm_item.get("item_code") == fg_item_code
):
rm_item_code = rm_item.get("rm_item_code")
items_dict = {
rm_item_code: {
rm_detail_field: rm_item.get("name"),
"item_name": rm_item.get("item_name")
or item_wh.get(rm_item_code, {}).get("item_name", ""),
"description": item_wh.get(rm_item_code, {}).get("description", ""),
"qty": rm_item.get("qty")
or max(
rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0
),
"from_warehouse": rm_item.get("warehouse")
or rm_item.get("reserve_warehouse"),
"to_warehouse": source_doc.supplier_warehouse,
"stock_uom": rm_item.get("stock_uom"),
"serial_and_batch_bundle": rm_item.get("serial_and_batch_bundle"),
"main_item_code": fg_item_code,
"allow_alternative_item": item_wh.get(rm_item_code, {}).get(
"allow_alternative_item"
),
"use_serial_batch_fields": rm_item.get("use_serial_batch_fields"),
"serial_no": rm_item.get("serial_no")
if rm_item.get("use_serial_batch_fields")
else None,
"batch_no": rm_item.get("batch_no")
if rm_item.get("use_serial_batch_fields")
else None,
}
}
target_doc.add_to_stock_entry_detail(items_dict)
stock_entry = get_mapped_doc( stock_entry = get_mapped_doc(
order_doctype, order_doctype,
subcontract_order.name, subcontract_order.name,
@@ -1317,53 +1366,9 @@ def make_rm_stock_entry(
}, },
target_doc, target_doc,
ignore_child_tables=True, ignore_child_tables=True,
postprocess=post_process,
) )
stock_entry.purpose = "Send to Subcontractor"
if order_doctype == "Purchase Order":
stock_entry.purchase_order = subcontract_order.name
else:
stock_entry.subcontracting_order = subcontract_order.name
stock_entry.set_stock_entry_type()
for fg_item_code in fg_item_code_list:
for rm_item in rm_items:
if (
rm_item.get("main_item_code") == fg_item_code
or rm_item.get("item_code") == fg_item_code
):
rm_item_code = rm_item.get("rm_item_code")
items_dict = {
rm_item_code: {
rm_detail_field: rm_item.get("name"),
"item_name": rm_item.get("item_name")
or item_wh.get(rm_item_code, {}).get("item_name", ""),
"description": item_wh.get(rm_item_code, {}).get("description", ""),
"qty": rm_item.get("qty")
or max(rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0),
"from_warehouse": rm_item.get("warehouse")
or rm_item.get("reserve_warehouse"),
"to_warehouse": subcontract_order.supplier_warehouse,
"stock_uom": rm_item.get("stock_uom"),
"serial_and_batch_bundle": rm_item.get("serial_and_batch_bundle"),
"main_item_code": fg_item_code,
"allow_alternative_item": item_wh.get(rm_item_code, {}).get(
"allow_alternative_item"
),
"use_serial_batch_fields": rm_item.get("use_serial_batch_fields"),
"serial_no": rm_item.get("serial_no")
if rm_item.get("use_serial_batch_fields")
else None,
"batch_no": rm_item.get("batch_no")
if rm_item.get("use_serial_batch_fields")
else None,
}
}
stock_entry.add_to_stock_entry_detail(items_dict)
if target_doc: if target_doc:
return stock_entry return stock_entry
else: else:
@@ -1395,6 +1400,8 @@ def add_items_in_ste(ste_doc, row, qty, rm_details, rm_detail_field="sco_rm_deta
def make_return_stock_entry_for_subcontract( def make_return_stock_entry_for_subcontract(
available_materials, order_doc, rm_details, order_doctype="Subcontracting Order" available_materials, order_doc, rm_details, order_doctype="Subcontracting Order"
): ):
rm_detail_field = "po_detail" if order_doctype == "Purchase Order" else "sco_rm_detail"
def post_process(source_doc, target_doc): def post_process(source_doc, target_doc):
target_doc.purpose = "Material Transfer" target_doc.purpose = "Material Transfer"
@@ -1405,6 +1412,21 @@ def make_return_stock_entry_for_subcontract(
target_doc.company = source_doc.company target_doc.company = source_doc.company
target_doc.is_return = 1 target_doc.is_return = 1
for _key, value in available_materials.items():
if not value.qty:
continue
if item_details := value.get("item_details"):
item_details["serial_and_batch_bundle"] = None
if value.batch_no:
for batch_no, qty in value.batch_no.items():
if qty > 0:
add_items_in_ste(target_doc, value, qty, rm_details, rm_detail_field, batch_no)
else:
add_items_in_ste(target_doc, value, value.qty, rm_details, rm_detail_field)
target_doc.set_stock_entry_type()
ste_doc = get_mapped_doc( ste_doc = get_mapped_doc(
order_doctype, order_doctype,
@@ -1419,27 +1441,6 @@ def make_return_stock_entry_for_subcontract(
postprocess=post_process, postprocess=post_process,
) )
if order_doctype == "Purchase Order":
rm_detail_field = "po_detail"
else:
rm_detail_field = "sco_rm_detail"
for _key, value in available_materials.items():
if not value.qty:
continue
if item_details := value.get("item_details"):
item_details["serial_and_batch_bundle"] = None
if value.batch_no:
for batch_no, qty in value.batch_no.items():
if qty > 0:
add_items_in_ste(ste_doc, value, qty, rm_details, rm_detail_field, batch_no)
else:
add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field)
ste_doc.set_stock_entry_type()
return ste_doc return ste_doc

View File

@@ -1076,9 +1076,9 @@ class JobCard(Document):
def is_work_order_closed(self): def is_work_order_closed(self):
if self.work_order: if self.work_order:
status = frappe.get_value("Work Order", self.work_order) status = frappe.get_value("Work Order", self.work_order, "status")
if status == "Closed": if status in ["Closed", "Stopped"]:
return True return True
return False return False

View File

@@ -425,10 +425,11 @@ frappe.ui.form.on("Work Order", {
var added_min = false; var added_min = false;
// produced qty // produced qty
var title = __("{0} items produced", [frm.doc.produced_qty]); let produced_qty = frm.doc.produced_qty - frm.doc.disassembled_qty;
var title = __("{0} items produced", [produced_qty]);
bars.push({ bars.push({
title: title, title: title,
width: (frm.doc.produced_qty / frm.doc.qty) * 100 + "%", width: (flt(produced_qty) / frm.doc.qty) * 100 + "%",
progress_class: "progress-bar-success", progress_class: "progress-bar-success",
}); });
if (bars[0].width == "0%") { if (bars[0].width == "0%") {
@@ -445,14 +446,27 @@ frappe.ui.form.on("Work Order", {
if (pending_complete > 0) { if (pending_complete > 0) {
var width = (pending_complete / frm.doc.qty) * 100 - added_min; var width = (pending_complete / frm.doc.qty) * 100 - added_min;
title = __("{0} items in progress", [pending_complete]); title = __("{0} items in progress", [pending_complete]);
let progress_class = "progress-bar-warning";
if (frm.doc.status == "Closed") {
if (frm.doc.required_items.find((d) => d.returned_qty > 0)) {
title = __("{0} items returned", [pending_complete]);
progress_class = "progress-bar-warning";
} else {
title = __("{0} items to return", [pending_complete]);
progress_class = "progress-bar-info";
}
}
bars.push({ bars.push({
title: title, title: title,
width: (width > 100 ? "99.5" : width) + "%", width: (width > 100 ? "99.5" : width) + "%",
progress_class: "progress-bar-warning", progress_class: progress_class,
}); });
message = message + ". " + title; message = message + ". " + title;
} }
} }
//process loss qty
if (frm.doc.process_loss_qty) { if (frm.doc.process_loss_qty) {
var process_loss_width = (frm.doc.process_loss_qty / frm.doc.qty) * 100; var process_loss_width = (frm.doc.process_loss_qty / frm.doc.qty) * 100;
title = __("{0} items lost during process.", [frm.doc.process_loss_qty]); title = __("{0} items lost during process.", [frm.doc.process_loss_qty]);
@@ -463,6 +477,19 @@ frappe.ui.form.on("Work Order", {
}); });
message = message + ". " + title; message = message + ". " + title;
} }
// disassembled qty
if (frm.doc.disassembled_qty) {
var disassembled_width = (frm.doc.disassembled_qty / frm.doc.qty) * 100;
title = __("{0} items disassembled", [frm.doc.disassembled_qty]);
bars.push({
title: title,
width: disassembled_width + "%",
progress_class: "progress-bar-secondary",
});
message = message + ". " + title;
}
frm.dashboard.add_progress(__("Status"), bars, message); frm.dashboard.add_progress(__("Status"), bars, message);
}, },

View File

@@ -220,39 +220,52 @@ class WorkOrder(Document):
) )
def validate_sales_order(self): def validate_sales_order(self):
if self.production_plan_sub_assembly_item:
return
if self.sales_order: if self.sales_order:
self.check_sales_order_on_hold_or_close() self.check_sales_order_on_hold_or_close()
so = frappe.db.sql(
""" SalesOrder = frappe.qb.DocType("Sales Order")
select so.name, so_item.delivery_date, so.project SalesOrderItem = frappe.qb.DocType("Sales Order Item")
from `tabSales Order` so PackedItem = frappe.qb.DocType("Packed Item")
inner join `tabSales Order Item` so_item on so_item.parent = so.name ProductBundleItem = frappe.qb.DocType("Product Bundle Item")
left join `tabProduct Bundle Item` pk_item on so_item.item_code = pk_item.parent
where so.name=%s and so.docstatus = 1 so = (
and so.skip_delivery_note = 0 and ( frappe.qb.from_(SalesOrder)
so_item.item_code=%s or .inner_join(SalesOrderItem)
pk_item.item_code=%s ) .on(SalesOrderItem.parent == SalesOrder.name)
""", .left_join(ProductBundleItem)
(self.sales_order, self.production_item, self.production_item), .on(ProductBundleItem.parent == SalesOrderItem.item_code)
as_dict=1, .select(SalesOrder.name, SalesOrder.project, SalesOrderItem.delivery_date)
.where(
(SalesOrder.skip_delivery_note == 0)
& (SalesOrder.docstatus == 1)
& (SalesOrder.name == self.sales_order)
& (
(SalesOrderItem.item_code == self.production_item)
| (ProductBundleItem.item_code == self.production_item)
)
)
.run(as_dict=1)
) )
if not so: if not so:
so = frappe.db.sql( so = (
""" frappe.qb.from_(SalesOrder)
select .inner_join(SalesOrderItem)
so.name, so_item.delivery_date, so.project .on(SalesOrderItem.parent == SalesOrder.name)
from .inner_join(PackedItem)
`tabSales Order` so, `tabSales Order Item` so_item, `tabPacked Item` packed_item .on(PackedItem.parent == SalesOrder.name)
where so.name=%s .select(SalesOrder.name, SalesOrder.project, SalesOrderItem.delivery_date)
and so.name=so_item.parent .where(
and so.name=packed_item.parent (SalesOrder.name == self.sales_order)
and so.skip_delivery_note = 0 & (SalesOrder.skip_delivery_note == 0)
and so_item.item_code = packed_item.parent_item & (SalesOrderItem.item_code == PackedItem.parent_item)
and so.docstatus = 1 and packed_item.item_code=%s & (SalesOrder.docstatus == 1)
""", & (PackedItem.item_code == self.production_item)
(self.sales_order, self.production_item), )
as_dict=1, .run(as_dict=1)
) )
if len(so): if len(so):
@@ -426,7 +439,7 @@ class WorkOrder(Document):
from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item
if self.sales_order and self.sales_order_item: if self.sales_order and self.sales_order_item and not self.production_plan_sub_assembly_item:
update_produced_qty_in_so_item(self.sales_order, self.sales_order_item) update_produced_qty_in_so_item(self.sales_order, self.sales_order_item)
if self.production_plan: if self.production_plan:
@@ -818,7 +831,7 @@ class WorkOrder(Document):
doc.db_set("status", doc.status) doc.db_set("status", doc.status)
def update_work_order_qty_in_so(self): def update_work_order_qty_in_so(self):
if not self.sales_order and not self.sales_order_item: if (not self.sales_order and not self.sales_order_item) or self.production_plan_sub_assembly_item:
return return
total_bundle_qty = 1 total_bundle_qty = 1

View File

@@ -75,7 +75,6 @@ erpnext.patches.v12_0.make_item_manufacturer
erpnext.patches.v12_0.move_item_tax_to_item_tax_template erpnext.patches.v12_0.move_item_tax_to_item_tax_template
erpnext.patches.v11_1.set_variant_based_on erpnext.patches.v11_1.set_variant_based_on
erpnext.patches.v11_1.woocommerce_set_creation_user erpnext.patches.v11_1.woocommerce_set_creation_user
erpnext.patches.v11_1.rename_depends_on_lwp
execute:frappe.delete_doc("Report", "Inactive Items") execute:frappe.delete_doc("Report", "Inactive Items")
erpnext.patches.v11_1.delete_scheduling_tool erpnext.patches.v11_1.delete_scheduling_tool
erpnext.patches.v12_0.rename_tolerance_fields erpnext.patches.v12_0.rename_tolerance_fields
@@ -432,3 +431,4 @@ erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges
erpnext.patches.v16_0.set_ordered_qty_in_quotation_item erpnext.patches.v16_0.set_ordered_qty_in_quotation_item
erpnext.patches.v15_0.replace_http_with_https_in_sales_partner erpnext.patches.v15_0.replace_http_with_https_in_sales_partner
erpnext.patches.v16_0.add_portal_redirects erpnext.patches.v16_0.add_portal_redirects
erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po

View File

@@ -0,0 +1,33 @@
import frappe
from frappe.query_builder import DocType
from frappe.query_builder.functions import Sum
def execute():
PurchaseOrderItem = DocType("Purchase Order Item")
MaterialRequestItem = DocType("Material Request Item")
poi_query = (
frappe.qb.from_(PurchaseOrderItem)
.select(PurchaseOrderItem.sales_order_item, Sum(PurchaseOrderItem.stock_qty))
.where(PurchaseOrderItem.sales_order_item.isnotnull() & PurchaseOrderItem.docstatus == 1)
.groupby(PurchaseOrderItem.sales_order_item)
)
mri_query = (
frappe.qb.from_(MaterialRequestItem)
.select(MaterialRequestItem.sales_order_item, Sum(MaterialRequestItem.stock_qty))
.where(MaterialRequestItem.sales_order_item.isnotnull() & MaterialRequestItem.docstatus == 1)
.groupby(MaterialRequestItem.sales_order_item)
)
poi_data = poi_query.run()
mri_data = mri_query.run()
updates_against_poi = {data[0]: {"ordered_qty": data[1]} for data in poi_data}
updates_against_mri = {data[0]: {"requested_qty": data[1], "ordered_qty": 0} for data in mri_data}
frappe.db.auto_commit_on_many_writes = 1
frappe.db.bulk_update("Sales Order Item", updates_against_mri)
frappe.db.bulk_update("Sales Order Item", updates_against_poi)
frappe.db.auto_commit_on_many_writes = 0

View File

@@ -260,6 +260,33 @@ frappe.ui.form.on("Timesheet", {
parent_project: function (frm) { parent_project: function (frm) {
set_project_in_timelog(frm); set_project_in_timelog(frm);
}, },
employee: function (frm) {
if (frm.doc.employee && frm.doc.time_logs) {
const selected_employee = frm.doc.employee;
frm.doc.time_logs.forEach((row) => {
if (row.activity_type) {
frappe.call({
method: "erpnext.projects.doctype.timesheet.timesheet.get_activity_cost",
args: {
employee: frm.doc.employee,
activity_type: row.activity_type,
currency: frm.doc.currency,
},
callback: function (r) {
if (r.message) {
if (selected_employee !== frm.doc.employee) return;
row.billing_rate = r.message["billing_rate"];
row.costing_rate = r.message["costing_rate"];
frm.refresh_fields("time_logs");
calculate_billing_costing_amount(frm, row.doctype, row.name);
}
},
});
}
});
}
},
}); });
frappe.ui.form.on("Timesheet Detail", { frappe.ui.form.on("Timesheet Detail", {

View File

@@ -173,9 +173,15 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if (!tax.dont_recompute_tax) { if (!tax.dont_recompute_tax) {
tax.item_wise_tax_detail = {}; tax.item_wise_tax_detail = {};
} }
var tax_fields = ["total", "tax_amount_after_discount_amount", var tax_fields = [
"tax_amount_for_current_item", "grand_total_for_current_item", "net_amount",
"tax_fraction_for_current_item", "grand_total_fraction_for_current_item"]; "total",
"tax_amount_after_discount_amount",
"tax_amount_for_current_item",
"grand_total_for_current_item",
"tax_fraction_for_current_item",
"grand_total_fraction_for_current_item",
];
if (cstr(tax.charge_type) != "Actual" && if (cstr(tax.charge_type) != "Actual" &&
!(me.discount_amount_applied && me.frm.doc.apply_discount_on=="Grand Total")) { !(me.discount_amount_applied && me.frm.doc.apply_discount_on=="Grand Total")) {
@@ -363,9 +369,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate); var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
$.each(doc.taxes, function(i, tax) { $.each(doc.taxes, function(i, tax) {
// tax_amount represents the amount of tax for the current step // tax_amount represents the amount of tax for the current step
var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map); var [current_net_amount, current_tax_amount] = me.get_current_tax_amount(
item,
tax,
item_tax_map
);
if (frappe.flags.round_row_wise_tax) { if (frappe.flags.round_row_wise_tax) {
current_tax_amount = flt(current_tax_amount, precision("tax_amount", tax)); current_tax_amount = flt(current_tax_amount, precision("tax_amount", tax));
current_net_amount = flt(current_net_amount, precision("net_amount", tax));
} }
// Adjust divisional loss to the last item // Adjust divisional loss to the last item
@@ -380,6 +391,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if (tax.charge_type != "Actual" && if (tax.charge_type != "Actual" &&
!(me.discount_amount_applied && me.frm.doc.apply_discount_on=="Grand Total")) { !(me.discount_amount_applied && me.frm.doc.apply_discount_on=="Grand Total")) {
tax.tax_amount += current_tax_amount; tax.tax_amount += current_tax_amount;
tax.net_amount += current_net_amount;
} }
// store tax_amount for current item as it will be used for // store tax_amount for current item as it will be used for
@@ -430,8 +442,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
for (const [i, tax] of doc.taxes.entries()) { for (const [i, tax] of doc.taxes.entries()) {
me.round_off_totals(tax); me.round_off_totals(tax);
me.set_in_company_currency(tax, me.set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount", "net_amount"]);
["tax_amount", "tax_amount_after_discount_amount"]);
me.round_off_base_values(tax); me.round_off_base_values(tax);
@@ -464,6 +475,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
get_current_tax_amount(item, tax, item_tax_map) { get_current_tax_amount(item, tax, item_tax_map) {
var tax_rate = this._get_tax_rate(tax, item_tax_map); var tax_rate = this._get_tax_rate(tax, item_tax_map);
var current_tax_amount = 0.0; var current_tax_amount = 0.0;
var current_net_amount = 0.0;
// To set row_id by default as previous row. // To set row_id by default as previous row.
if(["On Previous Row Amount", "On Previous Row Total"].includes(tax.charge_type)) { if(["On Previous Row Amount", "On Previous Row Total"].includes(tax.charge_type)) {
@@ -476,21 +488,27 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
} }
} }
if(tax.charge_type == "Actual") { if(tax.charge_type == "Actual") {
current_net_amount = item.net_amount
// distribute the tax amount proportionally to each item row // distribute the tax amount proportionally to each item row
var actual = flt(tax.tax_amount, precision("tax_amount", tax)); var actual = flt(tax.tax_amount, precision("tax_amount", tax));
current_tax_amount = this.frm.doc.net_total ? current_tax_amount = this.frm.doc.net_total ?
((item.net_amount / this.frm.doc.net_total) * actual) : 0.0; ((item.net_amount / this.frm.doc.net_total) * actual) : 0.0;
} else if(tax.charge_type == "On Net Total") { } else if(tax.charge_type == "On Net Total") {
if (tax.account_head in item_tax_map) {
current_net_amount = item.net_amount
};
current_tax_amount = (tax_rate / 100.0) * item.net_amount; current_tax_amount = (tax_rate / 100.0) * item.net_amount;
} else if(tax.charge_type == "On Previous Row Amount") { } else if(tax.charge_type == "On Previous Row Amount") {
current_net_amount = this.frm.doc["taxes"][cint(tax.row_id) - 1].tax_amount_for_current_item
current_tax_amount = (tax_rate / 100.0) * current_tax_amount = (tax_rate / 100.0) *
this.frm.doc["taxes"][cint(tax.row_id) - 1].tax_amount_for_current_item; this.frm.doc["taxes"][cint(tax.row_id) - 1].tax_amount_for_current_item;
} else if(tax.charge_type == "On Previous Row Total") { } else if(tax.charge_type == "On Previous Row Total") {
current_net_amount = this.frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_for_current_item
current_tax_amount = (tax_rate / 100.0) * current_tax_amount = (tax_rate / 100.0) *
this.frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_for_current_item; this.frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_for_current_item;
} else if (tax.charge_type == "On Item Quantity") { } else if (tax.charge_type == "On Item Quantity") {
// don't sum current net amount due to the field being a currency field
current_tax_amount = tax_rate * item.qty; current_tax_amount = tax_rate * item.qty;
} }
@@ -498,7 +516,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount); this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount);
} }
return current_tax_amount; return [current_net_amount, current_tax_amount];
} }
set_item_wise_tax(item, tax, tax_rate, current_tax_amount) { set_item_wise_tax(item, tax, tax_rate, current_tax_amount) {
@@ -532,7 +550,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
} }
tax.tax_amount = flt(tax.tax_amount, precision("tax_amount", tax)); tax.tax_amount = flt(tax.tax_amount, precision("tax_amount", tax));
tax.tax_amount_after_discount_amount = flt(tax.tax_amount_after_discount_amount, precision("tax_amount", tax)); tax.net_amount = flt(tax.net_amount, precision("net_amount", tax));
tax.tax_amount_after_discount_amount = flt(
tax.tax_amount_after_discount_amount,
precision("tax_amount", tax)
);
} }
round_off_base_values(tax) { round_off_base_values(tax) {

View File

@@ -52,8 +52,22 @@ class TestQuotation(FrappeTestCase):
self.assertEqual(qo.get("items")[0].qty, 11) self.assertEqual(qo.get("items")[0].qty, 11)
self.assertEqual(qo.get("items")[-1].rate, 100) self.assertEqual(qo.get("items")[-1].rate, 100)
def test_update_child_disallow_rate_change(self): def test_update_child_rate_change(self):
qo = make_quotation(qty=4) from erpnext.stock.doctype.item.test_item import make_item
item_1 = make_item("_Test Item")
item_2 = make_item("_Test Item 1")
item_list = [
{"item_code": item_1.item_code, "warehouse": "_Test Warehouse - _TC", "qty": 10, "rate": 300},
{"item_code": item_2.item_code, "warehouse": "_Test Warehouse - _TC", "qty": 5, "rate": 400},
]
qo = make_quotation(item_list=item_list)
so = make_sales_order(qo.name, args={"filtered_children": [qo.items[0].name]})
so.delivery_date = nowdate()
so.submit()
qo.reload()
trans_item = json.dumps( trans_item = json.dumps(
[ [
{ {
@@ -61,10 +75,35 @@ class TestQuotation(FrappeTestCase):
"rate": 5000, "rate": 5000,
"qty": qo.items[0].qty, "qty": qo.items[0].qty,
"docname": qo.items[0].name, "docname": qo.items[0].name,
} },
{
"item_code": qo.items[1].item_code,
"rate": qo.items[1].rate,
"qty": qo.items[1].qty,
"docname": qo.items[1].name,
},
] ]
) )
self.assertRaises(frappe.ValidationError, update_child_qty_rate, "Quotation", trans_item, qo.name) self.assertRaises(frappe.ValidationError, update_child_qty_rate, "Quotation", trans_item, qo.name)
trans_item = json.dumps(
[
{
"item_code": qo.items[0].item_code,
"rate": qo.items[0].rate,
"qty": qo.items[0].qty,
"docname": qo.items[0].name,
},
{
"item_code": qo.items[1].item_code,
"rate": 50,
"qty": qo.items[1].qty,
"docname": qo.items[1].name,
},
]
)
update_child_qty_rate("Quotation", trans_item, qo.name)
qo.reload()
self.assertEqual(qo.items[1].rate, 50)
def test_update_child_removing_item(self): def test_update_child_removing_item(self):
qo = make_quotation(qty=10) qo = make_quotation(qty=10)

View File

@@ -56,6 +56,13 @@ frappe.ui.form.on("Sales Order", {
frm.set_df_property("packed_items", "cannot_add_rows", true); frm.set_df_property("packed_items", "cannot_add_rows", true);
frm.set_df_property("packed_items", "cannot_delete_rows", true); frm.set_df_property("packed_items", "cannot_delete_rows", true);
}, },
delivery_date(frm) {
if (frm.doc.delivery_date) {
frm.doc.items.forEach((d) => {
frappe.model.set_value(d.doctype, d.name, "delivery_date", frm.doc.delivery_date);
});
}
},
refresh: function (frm) { refresh: function (frm) {
if (frm.doc.docstatus === 1) { if (frm.doc.docstatus === 1) {
@@ -145,7 +152,7 @@ frappe.ui.form.on("Sales Order", {
}); });
} }
} }
prevent_past_delivery_dates(frm);
// Hide `Reserve Stock` field description in submitted or cancelled Sales Order. // Hide `Reserve Stock` field description in submitted or cancelled Sales Order.
if (frm.doc.docstatus > 0) { if (frm.doc.docstatus > 0) {
frm.set_df_property("reserve_stock", "description", null); frm.set_df_property("reserve_stock", "description", null);
@@ -224,13 +231,6 @@ frappe.ui.form.on("Sales Order", {
]; ];
}, },
delivery_date: function (frm) {
$.each(frm.doc.items || [], function (i, d) {
if (!d.delivery_date) d.delivery_date = frm.doc.delivery_date;
});
refresh_field("items");
},
create_stock_reservation_entries(frm) { create_stock_reservation_entries(frm) {
const dialog = new frappe.ui.Dialog({ const dialog = new frappe.ui.Dialog({
title: __("Stock Reservation"), title: __("Stock Reservation"),
@@ -1400,3 +1400,11 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}; };
extend_cscript(cur_frm.cscript, new erpnext.selling.SalesOrderController({ frm: cur_frm })); extend_cscript(cur_frm.cscript, new erpnext.selling.SalesOrderController({ frm: cur_frm }));
function prevent_past_delivery_dates(frm) {
if (frm.doc.transaction_date) {
frm.fields_dict["delivery_date"].datepicker?.update({
minDate: new Date(frm.doc.transaction_date),
});
}
}

View File

@@ -90,6 +90,7 @@
"ordered_qty", "ordered_qty",
"planned_qty", "planned_qty",
"production_plan_qty", "production_plan_qty",
"requested_qty",
"column_break_69", "column_break_69",
"work_order_qty", "work_order_qty",
"delivered_qty", "delivered_qty",
@@ -966,12 +967,56 @@
"label": "Project", "label": "Project",
"options": "Project", "options": "Project",
"search_index": 1 "search_index": 1
},
{
"fieldname": "sales_order_schedule_section",
"fieldtype": "Section Break",
"label": "Sales Order Schedule"
},
{
"fieldname": "add_schedule",
"fieldtype": "Button",
"label": "Add Schedule"
},
{
"allow_on_submit": 1,
"default": "0",
"depends_on": "eval:parent.is_subcontracted",
"fieldname": "subcontracted_qty",
"fieldtype": "Float",
"label": "Subcontracted Quantity",
"no_copy": 1,
"non_negative": 1,
"read_only": 1
},
{
"depends_on": "eval:parent.is_subcontracted",
"fieldname": "fg_item",
"fieldtype": "Link",
"label": "Finished Good",
"mandatory_depends_on": "eval:parent.is_subcontracted",
"options": "Item"
},
{
"depends_on": "eval:parent.is_subcontracted",
"fieldname": "fg_item_qty",
"fieldtype": "Float",
"label": "Finished Good Qty",
"mandatory_depends_on": "eval:parent.is_subcontracted"
},
{
"fieldname": "requested_qty",
"fieldtype": "Float",
"label": "Requested Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2026-02-20 16:39:00.200328", "modified": "2026-02-21 16:39:00.200328",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Item", "name": "Sales Order Item",

View File

@@ -78,6 +78,7 @@ class SalesOrderItem(Document):
quotation_item: DF.Data | None quotation_item: DF.Data | None
rate: DF.Currency rate: DF.Currency
rate_with_margin: DF.Currency rate_with_margin: DF.Currency
requested_qty: DF.Float
reserve_stock: DF.Check reserve_stock: DF.Check
returned_qty: DF.Float returned_qty: DF.Float
stock_qty: DF.Float stock_qty: DF.Float

View File

@@ -81,7 +81,7 @@ class MaterialRequest(BuyingController):
{ {
"source_dt": "Material Request Item", "source_dt": "Material Request Item",
"target_dt": "Sales Order Item", "target_dt": "Sales Order Item",
"target_field": "ordered_qty", "target_field": "requested_qty",
"target_parent_dt": "Sales Order", "target_parent_dt": "Sales Order",
"target_parent_field": "", "target_parent_field": "",
"join_field": "sales_order_item", "join_field": "sales_order_item",
@@ -248,6 +248,8 @@ class MaterialRequest(BuyingController):
def on_cancel(self): def on_cancel(self):
self.update_requested_qty_in_production_plan() self.update_requested_qty_in_production_plan()
self.update_requested_qty() self.update_requested_qty()
if self.material_request_type == "Purchase":
self.update_prevdoc_status()
def get_mr_items_ordered_qty(self, mr_items): def get_mr_items_ordered_qty(self, mr_items):
mr_items_ordered_qty = {} mr_items_ordered_qty = {}

View File

@@ -333,7 +333,7 @@ class SerialBatchBundle:
"Serial and Batch Entry", {"parent": self.sle.serial_and_batch_bundle, "docstatus": 0} "Serial and Batch Entry", {"parent": self.sle.serial_and_batch_bundle, "docstatus": 0}
) )
> 0 > 0
): ) and not self.sle.is_cancelled:
frappe.throw( frappe.throw(
_("Serial and Batch Bundle {0} is not submitted").format( _("Serial and Batch Bundle {0} is not submitted").format(
bold(self.sle.serial_and_batch_bundle) bold(self.sle.serial_and_batch_bundle)

View File

@@ -8,7 +8,7 @@
<form action="/search_help" style="display: flex;"> <form action="/search_help" style="display: flex;">
<input name='q' class='form-control' type='text' <input name='q' class='form-control' type='text'
style='max-width: 400px; display: inline-block; margin-right: 10px;' style='max-width: 400px; display: inline-block; margin-right: 10px;'
value='{{ frappe.form_dict.q or ''}}' value='{{ (frappe.form_dict.q or '') | e }}'
{% if not frappe.form_dict.q%}placeholder="{{ _("What do you need help with?") }}"{% endif %}> {% if not frappe.form_dict.q%}placeholder="{{ _("What do you need help with?") }}"{% endif %}>
<input type='submit' <input type='submit'
class='btn btn-sm btn-light btn-search' value="{{ _("Search") }}"> class='btn btn-sm btn-light btn-search' value="{{ _("Search") }}">