Merge pull request #51305 from mihir-kandoi/gh45824

This commit is contained in:
Mihir Kandoi
2025-12-31 14:56:38 +05:30
committed by GitHub
parent cb5926f59b
commit 87d00a373f
5 changed files with 186 additions and 13 deletions

View File

@@ -1401,6 +1401,7 @@ def make_rm_stock_entry(
stock_entry.set_stock_entry_type()
over_transfer_allowance = frappe.get_single_value("Buying Settings", "over_transfer_allowance")
for fg_item_code in fg_item_code_list:
for rm_item in rm_items:
if (
@@ -1408,14 +1409,27 @@ def make_rm_stock_entry(
or rm_item.get("item_code") == fg_item_code
):
rm_item_code = rm_item.get("rm_item_code")
qty = rm_item.get("qty") or max(
rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0
)
if qty <= 0 and rm_item.get("total_supplied_qty"):
per_transferred = (
flt(
rm_item.get("total_supplied_qty") / rm_item.get("required_qty"),
frappe.db.get_default("float_precision"),
)
* 100
)
if per_transferred >= 100 + over_transfer_allowance:
continue
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),
"qty": qty,
"from_warehouse": rm_item.get("warehouse")
or rm_item.get("reserve_warehouse"),
"to_warehouse": subcontract_order.supplier_warehouse,

View File

@@ -1454,9 +1454,11 @@ class StockEntry(StockController, SubcontractingInwardController):
)
).run()[0][0] or 0
if flt(total_supplied - total_returned, precision) > flt(total_allowed, precision):
if flt(total_supplied + se_item.transfer_qty - total_returned, precision) > flt(
total_allowed, precision
):
frappe.throw(
_("Row {0}# Item {1} cannot be transferred more than {2} against {3} {4}").format(
_("Row #{0}: Item {1} cannot be transferred more than {2} against {3} {4}").format(
se_item.idx,
se_item.item_code,
total_allowed,
@@ -3080,7 +3082,7 @@ class StockEntry(StockController, SubcontractingInwardController):
child_qty = flt(item_row["qty"], precision)
if not self.is_return and child_qty <= 0 and not item_row.get("is_scrap_item"):
if self.purpose != "Receive from Customer":
if self.purpose not in ["Receive from Customer", "Send to Subcontractor"]:
continue
se_child = self.append("items")

View File

@@ -460,6 +460,119 @@ frappe.ui.form.on("Subcontracting Order", {
});
},
make_subcontracting_receipt(this_obj) {
const doc = this_obj.frm.doc;
const has_overtransferred_items = doc.supplied_items.some((item) => {
return item.supplied_qty > item.required_qty;
});
const backflush_based_on = doc.__onload.backflush_based_on;
if (has_overtransferred_items && backflush_based_on === "BOM") {
const raw_data = doc.supplied_items.map((item) => {
const row = doc.items.find((i) => i.name === item.reference_name);
const qty = flt(row.qty) - flt(row.received_qty);
return {
__checked: 1,
item_code: row.item_code,
warehouse: row.warehouse,
bom_no: row.bom,
required_by: row.schedule_date,
qty: qty > 0 ? qty : null,
subcontracting_order_item: row.name,
};
});
const item_names_list = [];
const data = [];
raw_data.forEach((d) => {
if (!item_names_list.includes(d.subcontracting_order_item)) {
item_names_list.push(d.subcontracting_order_item);
data.push(d);
}
});
const dialog = new frappe.ui.Dialog({
title: __("Select Items"),
size: "extra-large",
fields: [
{
fieldname: "items",
fieldtype: "Table",
reqd: 1,
label: __("Select Items to Receive"),
cannot_add_rows: true,
fields: [
{
fieldtype: "Link",
fieldname: "item_code",
reqd: 1,
options: "Item",
label: __("Item Code"),
in_list_view: 1,
read_only: 1,
},
{
fieldtype: "Link",
fieldname: "warehouse",
options: "Warehouse",
label: __("Warehouse"),
in_list_view: 1,
read_only: 1,
reqd: 1,
},
{
fieldtype: "Link",
fieldname: "bom_no",
options: "BOM",
label: __("BOM"),
in_list_view: 1,
read_only: 1,
reqd: 1,
},
{
fieldtype: "Date",
fieldname: "required_by",
label: __("Required By"),
in_list_view: 1,
read_only: 1,
reqd: 1,
},
{
fieldtype: "Float",
fieldname: "qty",
reqd: 1,
label: __("Qty to Receive"),
in_list_view: 1,
},
{
fieldtype: "Data",
fieldname: "subcontracting_order_item",
reqd: 1,
label: __("Subcontracting Order Item"),
hidden: 1,
read_only: 1,
in_list_view: 0,
},
],
data: data,
},
],
primary_action_label: __("Proceed"),
primary_action: () => {
const values = dialog.fields_dict["items"].grid
.get_selected_children()
.map((i) => ({ name: i.subcontracting_order_item, qty: i.qty }));
if (values.some((i) => !i.qty || i.qty == 0)) {
frappe.throw(__("Quantity is mandatory for the selected items."));
} else {
this_obj.make_subcontracting_receipt(values);
}
},
});
dialog.show();
} else {
this_obj.make_subcontracting_receipt();
}
},
company: function (frm) {
erpnext.utils.set_letter_head(frm);
},
@@ -524,11 +637,11 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll
var me = this;
if (doc.docstatus == 1) {
if (!["Closed", "Completed"].includes(doc.status)) {
if (flt(doc.per_received) < 100) {
if (doc.status != "Closed") {
if (flt(doc.per_received) < 100 + doc.__onload.over_delivery_receipt_allowance) {
this.frm.add_custom_button(
__("Subcontracting Receipt"),
this.make_subcontracting_receipt,
() => this.frm.events.make_subcontracting_receipt(this),
__("Create")
);
if (me.has_unsupplied_items()) {
@@ -576,10 +689,12 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll
});
}
make_subcontracting_receipt() {
make_subcontracting_receipt(items) {
frappe.model.open_mapped_doc({
method: "erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt",
frm: cur_frm,
args: { items: items || [] },
freeze: true,
freeze_message: __("Creating Subcontracting Receipt ..."),
});
}

View File

@@ -110,6 +110,14 @@ class SubcontractingOrder(SubcontractingController):
"over_transfer_allowance",
frappe.db.get_single_value("Buying Settings", "over_transfer_allowance"),
)
self.set_onload(
"over_delivery_receipt_allowance",
frappe.get_single_value("Stock Settings", "over_delivery_receipt_allowance"),
)
self.set_onload(
"backflush_based_on",
frappe.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on"),
)
if self.reserve_stock:
if self.has_unreserved_stock():
@@ -464,16 +472,18 @@ class SubcontractingOrder(SubcontractingController):
@frappe.whitelist()
def make_subcontracting_receipt(source_name, target_doc=None):
return get_mapped_subcontracting_receipt(source_name, target_doc)
items = frappe.flags.args.get("items") if frappe.flags.args else None
return get_mapped_subcontracting_receipt(source_name, target_doc, items=items)
def get_mapped_subcontracting_receipt(source_name, target_doc=None):
def get_mapped_subcontracting_receipt(source_name, target_doc=None, items=None):
def update_item(source, target, source_parent):
target.purchase_order = source_parent.purchase_order
target.purchase_order_item = source.purchase_order_item
target.qty = flt(source.qty) - flt(source.received_qty)
target.qty = items.get(source.name) or (flt(source.qty) - flt(source.received_qty))
target.amount = (flt(source.qty) - flt(source.received_qty)) * flt(source.rate)
items = {item["name"]: item["qty"] for item in items} if items else {}
target_doc = get_mapped_doc(
"Subcontracting Order",
source_name,
@@ -496,7 +506,9 @@ def get_mapped_subcontracting_receipt(source_name, target_doc=None):
"bom": "bom",
},
"postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty),
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
if not items
else doc.name in items,
},
},
target_doc,

View File

@@ -1890,6 +1890,36 @@ class TestSubcontractingReceipt(IntegrationTestCase):
self.assertRaises(BOMQuantityError, scr.submit)
@IntegrationTestCase.change_settings("Buying Settings", {"over_transfer_allowance": 20})
@IntegrationTestCase.change_settings("Stock Settings", {"over_delivery_receipt_allowance": 20})
def test_over_receipt(self):
from erpnext.controllers.subcontracting_controller import make_rm_stock_entry
set_backflush_based_on("BOM")
sco = get_subcontracting_order()
rm_items = get_rm_items(sco.supplied_items)
itemwise_details = make_stock_in_entry(rm_items=rm_items)
make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
rm_items[0]["qty"] = 2
itemwise_details = make_stock_in_entry(rm_items=rm_items)
ste_dict = make_rm_stock_entry(sco.name)
doc = frappe.get_doc(ste_dict)
self.assertEqual(doc.items[0].qty, 0)
doc.items[0].qty = 2
doc.submit()
frappe.flags["args"] = {"items": [{"name": sco.items[0].name, "qty": 2}]}
scr = make_subcontracting_receipt(sco.name)
self.assertEqual(scr.items[0].qty, 2)
scr.submit()
frappe.flags["args"].pop("items", None)
def make_return_subcontracting_receipt(**args):
args = frappe._dict(args)