mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-07 23:31:20 +00:00
Merge pull request #51305 from mihir-kandoi/gh45824
This commit is contained in:
@@ -1401,6 +1401,7 @@ def make_rm_stock_entry(
|
|||||||
|
|
||||||
stock_entry.set_stock_entry_type()
|
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 fg_item_code in fg_item_code_list:
|
||||||
for rm_item in rm_items:
|
for rm_item in rm_items:
|
||||||
if (
|
if (
|
||||||
@@ -1408,14 +1409,27 @@ def make_rm_stock_entry(
|
|||||||
or rm_item.get("item_code") == fg_item_code
|
or rm_item.get("item_code") == fg_item_code
|
||||||
):
|
):
|
||||||
rm_item_code = rm_item.get("rm_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 = {
|
items_dict = {
|
||||||
rm_item_code: {
|
rm_item_code: {
|
||||||
rm_detail_field: rm_item.get("name"),
|
rm_detail_field: rm_item.get("name"),
|
||||||
"item_name": rm_item.get("item_name")
|
"item_name": rm_item.get("item_name")
|
||||||
or item_wh.get(rm_item_code, {}).get("item_name", ""),
|
or item_wh.get(rm_item_code, {}).get("item_name", ""),
|
||||||
"description": item_wh.get(rm_item_code, {}).get("description", ""),
|
"description": item_wh.get(rm_item_code, {}).get("description", ""),
|
||||||
"qty": rm_item.get("qty")
|
"qty": qty,
|
||||||
or max(rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0),
|
|
||||||
"from_warehouse": rm_item.get("warehouse")
|
"from_warehouse": rm_item.get("warehouse")
|
||||||
or rm_item.get("reserve_warehouse"),
|
or rm_item.get("reserve_warehouse"),
|
||||||
"to_warehouse": subcontract_order.supplier_warehouse,
|
"to_warehouse": subcontract_order.supplier_warehouse,
|
||||||
|
|||||||
@@ -1454,9 +1454,11 @@ class StockEntry(StockController, SubcontractingInwardController):
|
|||||||
)
|
)
|
||||||
).run()[0][0] or 0
|
).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(
|
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.idx,
|
||||||
se_item.item_code,
|
se_item.item_code,
|
||||||
total_allowed,
|
total_allowed,
|
||||||
@@ -3080,7 +3082,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
|||||||
|
|
||||||
child_qty = flt(item_row["qty"], precision)
|
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 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
|
continue
|
||||||
|
|
||||||
se_child = self.append("items")
|
se_child = self.append("items")
|
||||||
|
|||||||
@@ -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) {
|
company: function (frm) {
|
||||||
erpnext.utils.set_letter_head(frm);
|
erpnext.utils.set_letter_head(frm);
|
||||||
},
|
},
|
||||||
@@ -524,11 +637,11 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll
|
|||||||
var me = this;
|
var me = this;
|
||||||
|
|
||||||
if (doc.docstatus == 1) {
|
if (doc.docstatus == 1) {
|
||||||
if (!["Closed", "Completed"].includes(doc.status)) {
|
if (doc.status != "Closed") {
|
||||||
if (flt(doc.per_received) < 100) {
|
if (flt(doc.per_received) < 100 + doc.__onload.over_delivery_receipt_allowance) {
|
||||||
this.frm.add_custom_button(
|
this.frm.add_custom_button(
|
||||||
__("Subcontracting Receipt"),
|
__("Subcontracting Receipt"),
|
||||||
this.make_subcontracting_receipt,
|
() => this.frm.events.make_subcontracting_receipt(this),
|
||||||
__("Create")
|
__("Create")
|
||||||
);
|
);
|
||||||
if (me.has_unsupplied_items()) {
|
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({
|
frappe.model.open_mapped_doc({
|
||||||
method: "erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt",
|
method: "erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt",
|
||||||
frm: cur_frm,
|
frm: cur_frm,
|
||||||
|
args: { items: items || [] },
|
||||||
|
freeze: true,
|
||||||
freeze_message: __("Creating Subcontracting Receipt ..."),
|
freeze_message: __("Creating Subcontracting Receipt ..."),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,14 @@ class SubcontractingOrder(SubcontractingController):
|
|||||||
"over_transfer_allowance",
|
"over_transfer_allowance",
|
||||||
frappe.db.get_single_value("Buying Settings", "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.reserve_stock:
|
||||||
if self.has_unreserved_stock():
|
if self.has_unreserved_stock():
|
||||||
@@ -464,16 +472,18 @@ class SubcontractingOrder(SubcontractingController):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_subcontracting_receipt(source_name, target_doc=None):
|
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):
|
def update_item(source, target, source_parent):
|
||||||
target.purchase_order = source_parent.purchase_order
|
target.purchase_order = source_parent.purchase_order
|
||||||
target.purchase_order_item = source.purchase_order_item
|
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)
|
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(
|
target_doc = get_mapped_doc(
|
||||||
"Subcontracting Order",
|
"Subcontracting Order",
|
||||||
source_name,
|
source_name,
|
||||||
@@ -496,7 +506,9 @@ def get_mapped_subcontracting_receipt(source_name, target_doc=None):
|
|||||||
"bom": "bom",
|
"bom": "bom",
|
||||||
},
|
},
|
||||||
"postprocess": update_item,
|
"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,
|
target_doc,
|
||||||
|
|||||||
@@ -1890,6 +1890,36 @@ class TestSubcontractingReceipt(IntegrationTestCase):
|
|||||||
|
|
||||||
self.assertRaises(BOMQuantityError, scr.submit)
|
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):
|
def make_return_subcontracting_receipt(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
Reference in New Issue
Block a user