diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index a6af04f989a..c6daf7ead01 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -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, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 3d66e3f684d..4bb35a14b43 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -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") diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index f8661e59e4b..82331d41ad4 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -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 ..."), }); } diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 221396f64d4..ee9cf7a8ee5 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -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, diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 6003978835b..5bf2a6dd6fe 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -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)