mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-02 19:59:12 +00:00
Merge pull request #50235 from mihir-kandoi/sre-sco
feat: stock reservation for subcontracting order
This commit is contained in:
@@ -172,11 +172,279 @@ frappe.ui.form.on("Subcontracting Order", {
|
||||
__("Status")
|
||||
);
|
||||
}
|
||||
|
||||
if (frm.doc.reserve_stock) {
|
||||
if (frm.doc.status !== "Closed") {
|
||||
if (frm.doc.__onload && frm.doc.__onload.has_unreserved_stock) {
|
||||
frm.add_custom_button(
|
||||
__("Reserve"),
|
||||
() => frm.events.create_stock_reservation_entries(frm),
|
||||
__("Stock Reservation")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
frm.doc.__onload &&
|
||||
frm.doc.__onload.has_reserved_stock &&
|
||||
frappe.model.can_cancel("Stock Reservation Entry")
|
||||
) {
|
||||
frm.add_custom_button(
|
||||
__("Unreserve"),
|
||||
() => frm.events.cancel_stock_reservation_entries(frm),
|
||||
__("Stock Reservation")
|
||||
);
|
||||
}
|
||||
|
||||
frm.doc.supplied_items.forEach((item) => {
|
||||
if (
|
||||
flt(item.stock_reserved_qty) > 0 &&
|
||||
frappe.model.can_read("Stock Reservation Entry")
|
||||
) {
|
||||
frm.add_custom_button(
|
||||
__("Reserved Stock"),
|
||||
() => frm.events.show_reserved_stock(frm),
|
||||
__("Stock Reservation")
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
frm.trigger("get_materials_from_supplier");
|
||||
},
|
||||
|
||||
create_stock_reservation_entries(frm) {
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Stock Reservation"),
|
||||
size: "extra-large",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "items",
|
||||
fieldtype: "Table",
|
||||
label: __("Items to Reserve"),
|
||||
allow_bulk_edit: false,
|
||||
cannot_add_rows: true,
|
||||
cannot_delete_rows: true,
|
||||
data: [],
|
||||
fields: [
|
||||
{
|
||||
fieldname: "subcontracting_order_supplied_item",
|
||||
fieldtype: "Link",
|
||||
label: __("Subcontracting Order Supplied Item"),
|
||||
options: "Subcontracting Order Supplied Item",
|
||||
reqd: 1,
|
||||
in_list_view: 1,
|
||||
read_only: 1,
|
||||
get_query: () => {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_filtered_child_rows",
|
||||
filters: {
|
||||
parenttype: frm.doc.doctype,
|
||||
parent: frm.doc.name,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "rm_item_code",
|
||||
fieldtype: "Link",
|
||||
label: __("Item Code"),
|
||||
options: "Item",
|
||||
reqd: 1,
|
||||
read_only: 1,
|
||||
in_list_view: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "warehouse",
|
||||
fieldtype: "Link",
|
||||
label: __("Warehouse"),
|
||||
options: "Warehouse",
|
||||
reqd: 1,
|
||||
in_list_view: 1,
|
||||
read_only: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "qty_to_reserve",
|
||||
fieldtype: "Float",
|
||||
label: __("Qty"),
|
||||
reqd: 1,
|
||||
in_list_view: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Reserve Stock"),
|
||||
primary_action: () => {
|
||||
var data = { items: dialog.fields_dict.items.grid.get_selected_children() };
|
||||
|
||||
if (data.items && data.items.length > 0) {
|
||||
frappe.call({
|
||||
doc: frm.doc,
|
||||
method: "reserve_raw_materials",
|
||||
args: {
|
||||
items: data.items.map((item) => ({
|
||||
name: item.subcontracting_order_supplied_item,
|
||||
qty_to_reserve: item.qty_to_reserve,
|
||||
})),
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __("Reserving Stock..."),
|
||||
callback: (_) => {
|
||||
frm.reload_doc();
|
||||
},
|
||||
});
|
||||
|
||||
dialog.hide();
|
||||
} else {
|
||||
frappe.msgprint(__("Please select items to reserve."));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
frm.doc.supplied_items.forEach((item) => {
|
||||
let unreserved_qty =
|
||||
flt(item.required_qty) - flt(item.supplied_qty) - flt(item.stock_reserved_qty);
|
||||
|
||||
if (unreserved_qty > 0) {
|
||||
dialog.fields_dict.items.df.data.push({
|
||||
__checked: 1,
|
||||
subcontracting_order_supplied_item: item.name,
|
||||
rm_item_code: item.rm_item_code,
|
||||
warehouse: item.reserve_warehouse,
|
||||
qty_to_reserve: unreserved_qty,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
dialog.fields_dict.items.grid.refresh();
|
||||
dialog.show();
|
||||
},
|
||||
|
||||
cancel_stock_reservation_entries(frm) {
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Stock Unreservation"),
|
||||
size: "extra-large",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "sr_entries",
|
||||
fieldtype: "Table",
|
||||
label: __("Reserved Stock"),
|
||||
allow_bulk_edit: false,
|
||||
cannot_add_rows: true,
|
||||
cannot_delete_rows: true,
|
||||
in_place_edit: true,
|
||||
data: [],
|
||||
fields: [
|
||||
{
|
||||
fieldname: "sre",
|
||||
fieldtype: "Link",
|
||||
label: __("Stock Reservation Entry"),
|
||||
options: "Stock Reservation Entry",
|
||||
reqd: 1,
|
||||
read_only: 1,
|
||||
in_list_view: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "item_code",
|
||||
fieldtype: "Link",
|
||||
label: __("Item Code"),
|
||||
options: "Item",
|
||||
reqd: 1,
|
||||
read_only: 1,
|
||||
in_list_view: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "warehouse",
|
||||
fieldtype: "Link",
|
||||
label: __("Warehouse"),
|
||||
options: "Warehouse",
|
||||
reqd: 1,
|
||||
read_only: 1,
|
||||
in_list_view: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "qty",
|
||||
fieldtype: "Float",
|
||||
label: __("Qty"),
|
||||
reqd: 1,
|
||||
read_only: 1,
|
||||
in_list_view: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Unreserve Stock"),
|
||||
primary_action: () => {
|
||||
var data = { sr_entries: dialog.fields_dict.sr_entries.grid.get_selected_children() };
|
||||
|
||||
if (data.sr_entries && data.sr_entries.length > 0) {
|
||||
frappe.call({
|
||||
doc: frm.doc,
|
||||
method: "cancel_stock_reservation_entries",
|
||||
args: {
|
||||
sre_list: data.sr_entries.map((item) => item.sre),
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __("Unreserving Stock..."),
|
||||
callback: (_) => {
|
||||
frm.doc.__onload.has_reserved_stock = false;
|
||||
frm.reload_doc();
|
||||
},
|
||||
});
|
||||
|
||||
dialog.hide();
|
||||
} else {
|
||||
frappe.msgprint(__("Please select items to unreserve."));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
frappe
|
||||
.call({
|
||||
method: "erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry.get_stock_reservation_entries_for_voucher",
|
||||
args: {
|
||||
voucher_type: frm.doctype,
|
||||
voucher_no: frm.doc.name,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!r.exc && r.message) {
|
||||
r.message.forEach((sre) => {
|
||||
if (flt(sre.reserved_qty) > flt(sre.delivered_qty)) {
|
||||
dialog.fields_dict.sr_entries.df.data.push({
|
||||
sre: sre.name,
|
||||
item_code: sre.item_code,
|
||||
warehouse: sre.warehouse,
|
||||
qty: flt(sre.reserved_qty) - flt(sre.delivered_qty),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
dialog.fields_dict.sr_entries.grid.refresh();
|
||||
dialog.show();
|
||||
});
|
||||
},
|
||||
|
||||
show_reserved_stock(frm) {
|
||||
// Get the latest modified date from the items table.
|
||||
var to_date = moment(new Date(Math.max(...frm.doc.items.map((e) => new Date(e.modified))))).format(
|
||||
"YYYY-MM-DD"
|
||||
);
|
||||
|
||||
frappe.route_options = {
|
||||
company: frm.doc.company,
|
||||
from_date: frm.doc.transaction_date,
|
||||
to_date: to_date,
|
||||
voucher_type: frm.doc.doctype,
|
||||
voucher_no: frm.doc.name,
|
||||
};
|
||||
frappe.set_route("query-report", "Reserved Stock");
|
||||
},
|
||||
|
||||
update_subcontracting_order_status(frm, status) {
|
||||
frappe.call({
|
||||
method: "erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.update_subcontracting_order_status",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"service_items",
|
||||
"raw_materials_supplied_section",
|
||||
"set_reserve_warehouse",
|
||||
"reserve_stock",
|
||||
"supplied_items",
|
||||
"tab_address_and_contact",
|
||||
"supplier_address",
|
||||
@@ -62,7 +63,8 @@
|
||||
"select_print_heading",
|
||||
"column_break_43",
|
||||
"letter_head",
|
||||
"tab_connections"
|
||||
"tab_connections",
|
||||
"production_plan"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -471,6 +473,22 @@
|
||||
"no_copy": 1,
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "reserve_stock",
|
||||
"fieldtype": "Check",
|
||||
"label": "Reserve Stock",
|
||||
"no_copy": 1,
|
||||
"show_on_timeline": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "production_plan",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Production Plan",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
|
||||
@@ -8,6 +8,10 @@ from frappe.utils import flt
|
||||
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
StockReservation,
|
||||
has_reserved_stock,
|
||||
)
|
||||
from erpnext.stock.stock_balance import update_bin_qty
|
||||
from erpnext.stock.utils import get_bin
|
||||
|
||||
@@ -50,8 +54,10 @@ class SubcontractingOrder(SubcontractingController):
|
||||
letter_head: DF.Link | None
|
||||
naming_series: DF.Literal["SC-ORD-.YYYY.-"]
|
||||
per_received: DF.Percent
|
||||
production_plan: DF.Data | None
|
||||
project: DF.Link | None
|
||||
purchase_order: DF.Link
|
||||
reserve_stock: DF.Check
|
||||
schedule_date: DF.Date | None
|
||||
select_print_heading: DF.Link | None
|
||||
service_items: DF.Table[SubcontractingOrderServiceItem]
|
||||
@@ -105,6 +111,13 @@ class SubcontractingOrder(SubcontractingController):
|
||||
frappe.db.get_single_value("Buying Settings", "over_transfer_allowance"),
|
||||
)
|
||||
|
||||
if self.reserve_stock:
|
||||
if self.has_unreserved_stock():
|
||||
self.set_onload("has_unreserved_stock", True)
|
||||
|
||||
if has_reserved_stock(self.doctype, self.name):
|
||||
self.set_onload("has_reserved_stock", True)
|
||||
|
||||
def before_validate(self):
|
||||
super().before_validate()
|
||||
|
||||
@@ -121,6 +134,7 @@ class SubcontractingOrder(SubcontractingController):
|
||||
self.update_prevdoc_status()
|
||||
self.update_status()
|
||||
self.update_subcontracted_quantity_in_po()
|
||||
self.reserve_raw_materials()
|
||||
|
||||
def on_cancel(self):
|
||||
self.update_prevdoc_status()
|
||||
@@ -253,10 +267,10 @@ class SubcontractingOrder(SubcontractingController):
|
||||
if si.fg_item:
|
||||
item = frappe.get_doc("Item", si.fg_item)
|
||||
|
||||
qty, subcontracted_qty, fg_item_qty = frappe.db.get_value(
|
||||
qty, subcontracted_qty, fg_item_qty, production_plan_sub_assembly_item = frappe.db.get_value(
|
||||
"Purchase Order Item",
|
||||
si.purchase_order_item,
|
||||
["qty", "subcontracted_qty", "fg_item_qty"],
|
||||
["qty", "subcontracted_qty", "fg_item_qty", "production_plan_sub_assembly_item"],
|
||||
)
|
||||
available_qty = flt(qty) - flt(subcontracted_qty)
|
||||
|
||||
@@ -292,6 +306,7 @@ class SubcontractingOrder(SubcontractingController):
|
||||
"purchase_order_item": si.purchase_order_item,
|
||||
"material_request": si.material_request,
|
||||
"material_request_item": si.material_request_item,
|
||||
"production_plan_sub_assembly_item": production_plan_sub_assembly_item,
|
||||
}
|
||||
)
|
||||
else:
|
||||
@@ -362,6 +377,90 @@ class SubcontractingOrder(SubcontractingController):
|
||||
subcontracted_qty,
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def reserve_raw_materials(self, items=None, stock_entry=None):
|
||||
if self.reserve_stock:
|
||||
item_dict = {}
|
||||
|
||||
if items:
|
||||
item_dict = {d["name"]: d for d in items}
|
||||
items = [item for item in self.supplied_items if item.name in item_dict]
|
||||
|
||||
reservation_items = []
|
||||
is_transfer = False
|
||||
for item in items or self.supplied_items:
|
||||
data = frappe._dict(
|
||||
{
|
||||
"voucher_no": self.name,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_detail_no": item.name,
|
||||
"item_code": item.rm_item_code,
|
||||
"warehouse": item_dict.get(item.name, {}).get("warehouse", item.reserve_warehouse),
|
||||
"stock_qty": item_dict.get(item.name, {}).get("qty_to_reserve", item.required_qty),
|
||||
}
|
||||
)
|
||||
|
||||
if stock_entry:
|
||||
data.update(
|
||||
{
|
||||
"from_voucher_no": stock_entry,
|
||||
"from_voucher_type": "Stock Entry",
|
||||
"from_voucher_detail_no": item_dict[item.name]["reference_voucher_detail_no"],
|
||||
"serial_and_batch_bundles": item_dict[item.name]["serial_and_batch_bundles"],
|
||||
}
|
||||
)
|
||||
elif self.production_plan:
|
||||
fg_item = next(i for i in self.items if i.name == item.reference_name)
|
||||
if production_plan_sub_assembly_item := fg_item.production_plan_sub_assembly_item:
|
||||
from_voucher_detail_no, reserved_qty = frappe.get_value(
|
||||
"Material Request Plan Item",
|
||||
{
|
||||
"parent": self.production_plan,
|
||||
"item_code": item.rm_item_code,
|
||||
"warehouse": item.reserve_warehouse,
|
||||
"sub_assembly_item_reference": production_plan_sub_assembly_item,
|
||||
"docstatus": 1,
|
||||
},
|
||||
["name", "stock_reserved_qty"],
|
||||
)
|
||||
if flt(item.stock_reserved_qty) < reserved_qty:
|
||||
is_transfer = True
|
||||
data.update(
|
||||
{
|
||||
"from_voucher_no": self.production_plan,
|
||||
"from_voucher_type": "Production Plan",
|
||||
"from_voucher_detail_no": from_voucher_detail_no,
|
||||
}
|
||||
)
|
||||
|
||||
reservation_items.append(data)
|
||||
|
||||
sre = StockReservation(self, items=reservation_items, notify=True)
|
||||
if is_transfer:
|
||||
sre.transfer_reservation_entries_to(
|
||||
self.production_plan, from_doctype="Production Plan", to_doctype="Subcontracting Order"
|
||||
)
|
||||
else:
|
||||
if sre.make_stock_reservation_entries():
|
||||
frappe.msgprint(_("Stock Reservation Entries created"), alert=True, indicator="blue")
|
||||
|
||||
def has_unreserved_stock(self) -> bool:
|
||||
for item in self.get("supplied_items"):
|
||||
if item.required_qty - flt(item.supplied_qty) - flt(item.stock_reserved_qty) > 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_stock_reservation_entries(self, sre_list=None, notify=True) -> None:
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
cancel_stock_reservation_entries,
|
||||
)
|
||||
|
||||
cancel_stock_reservation_entries(
|
||||
voucher_type=self.doctype, voucher_no=self.name, sre_list=sre_list, notify=notify
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontracting_receipt(source_name, target_doc=None):
|
||||
|
||||
@@ -4,5 +4,15 @@ from frappe import _
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "subcontracting_order",
|
||||
"transactions": [{"label": _("Reference"), "items": ["Subcontracting Receipt", "Stock Entry"]}],
|
||||
"non_standard_fieldnames": {"Stock Reservation Entry": "voucher_no"},
|
||||
"transactions": [
|
||||
{
|
||||
"label": _("Reference"),
|
||||
"items": ["Subcontracting Receipt", "Stock Entry"],
|
||||
},
|
||||
{
|
||||
"label": _("Stock Reservation"),
|
||||
"items": ["Stock Reservation Entry"],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -700,6 +700,126 @@ class TestSubcontractingOrder(IntegrationTestCase):
|
||||
|
||||
self.assertEqual(sco.supplied_items[0].required_qty, 210.149)
|
||||
|
||||
def test_stock_reservation(self):
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
get_sre_details_for_voucher,
|
||||
)
|
||||
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 4",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": "Subcontracted Item SA4",
|
||||
"fg_item_qty": 10,
|
||||
}
|
||||
]
|
||||
|
||||
sco = get_subcontracting_order(service_items=service_items, do_not_submit=1)
|
||||
sco.reserve_stock = 1
|
||||
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
make_stock_in_entry(rm_items=rm_items)
|
||||
sco.submit()
|
||||
|
||||
sre_list = get_sre_details_for_voucher("Subcontracting Order", sco.name)
|
||||
self.assertTrue(len(sre_list) > 0)
|
||||
|
||||
se_dict = make_rm_stock_entry(sco.name)
|
||||
se = frappe.get_doc(se_dict)
|
||||
se.items[-1].use_serial_batch_fields = 1
|
||||
se.save()
|
||||
se.submit()
|
||||
sco.reload()
|
||||
|
||||
for sre in sre_list:
|
||||
self.assertEqual(frappe.get_value("Stock Reservation Entry", sre.name, "status"), "Closed")
|
||||
|
||||
make_subcontracting_receipt(sco.name).submit()
|
||||
for status in frappe.get_all(
|
||||
"Stock Reservation Entry", filters={"voucher_no": sco.name, "docstatus": 1}, pluck="status"
|
||||
)[:3]:
|
||||
self.assertEqual(status, "Delivered")
|
||||
|
||||
def test_stock_reservation_transfer(self):
|
||||
from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||
get_items_for_material_requests,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import create_production_plan
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
get_serial_batch_entries_for_voucher,
|
||||
get_sre_details_for_voucher,
|
||||
)
|
||||
|
||||
parent_fg = make_item()
|
||||
make_bom(
|
||||
item=parent_fg.name, raw_materials=["Subcontracted Item SA10"], rate=100, rm_qty=1, currency="INR"
|
||||
)
|
||||
|
||||
plan = create_production_plan(
|
||||
item_code=parent_fg.name,
|
||||
planned_qty=10,
|
||||
do_not_submit=True,
|
||||
reserve_stock=True,
|
||||
skip_available_sub_assembly_item=True,
|
||||
for_warehouse="_Test Warehouse - _TC",
|
||||
sub_assembly_warehouse="_Test Warehouse - _TC",
|
||||
skip_getting_mr_items=True,
|
||||
)
|
||||
plan.get_sub_assembly_items()
|
||||
plan.sub_assembly_items[0].supplier = "_Test Supplier"
|
||||
mr_items = get_items_for_material_requests(plan.as_dict())
|
||||
for d in mr_items:
|
||||
plan.append("mr_items", d)
|
||||
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse - _TC", item_code="Subcontracted SRM Item 1", qty=10, basic_rate=100
|
||||
)
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse - _TC", item_code="Subcontracted SRM Item 2", qty=10, basic_rate=100
|
||||
)
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse - _TC", item_code="Subcontracted SRM Item 3", qty=10, basic_rate=100
|
||||
)
|
||||
plan.submit()
|
||||
|
||||
sre_against_plan = get_sre_details_for_voucher("Production Plan", plan.name)
|
||||
sbe_pp_list = []
|
||||
for sre in sre_against_plan:
|
||||
sbe_pp_list.append(
|
||||
sorted(
|
||||
get_serial_batch_entries_for_voucher(sre.name),
|
||||
key=lambda x: x.get("serial_no") or x.get("batch_no") or "",
|
||||
)
|
||||
)
|
||||
|
||||
plan.make_work_order()
|
||||
po = frappe.get_doc(
|
||||
"Purchase Order",
|
||||
frappe.get_value("Purchase Order Item", {"production_plan": plan.name}, "parent"),
|
||||
)
|
||||
po.items[0].item_code = "Subcontracted Service Item 4"
|
||||
po.items[0].qty = 10
|
||||
po.submit()
|
||||
so = create_subcontracting_order(po_name=po.name, do_not_save=1)
|
||||
so.supplier_warehouse = "_Test Warehouse 1 - _TC"
|
||||
so.reserve_stock = True
|
||||
so.submit()
|
||||
so.reload()
|
||||
|
||||
sre_against_so = get_sre_details_for_voucher("Subcontracting Order", so.name)
|
||||
sbe_so_list = []
|
||||
for sre in sre_against_so:
|
||||
sbe_so_list.append(
|
||||
sorted(
|
||||
get_serial_batch_entries_for_voucher(sre.name),
|
||||
key=lambda x: x.get("serial_no") or x.get("batch_no") or "",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(sbe_pp_list, sbe_so_list)
|
||||
|
||||
|
||||
def create_subcontracting_order(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
"section_break_34",
|
||||
"purchase_order_item",
|
||||
"page_break",
|
||||
"subcontracting_conversion_factor"
|
||||
"subcontracting_conversion_factor",
|
||||
"production_plan_sub_assembly_item"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -407,6 +408,16 @@
|
||||
"hidden": 1,
|
||||
"label": "Subcontracting Conversion Factor",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "production_plan_sub_assembly_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Production Plan Sub Assembly Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"report_hide": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -414,7 +425,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-10 22:37:39.863628",
|
||||
"modified": "2025-11-03 12:29:45.156101",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Order Item",
|
||||
|
||||
@@ -35,6 +35,7 @@ class SubcontractingOrderItem(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
production_plan_sub_assembly_item: DF.Data | None
|
||||
project: DF.Link | None
|
||||
purchase_order_item: DF.Data | None
|
||||
qty: DF.Float
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"section_break_13",
|
||||
"required_qty",
|
||||
"supplied_qty",
|
||||
"stock_reserved_qty",
|
||||
"column_break_16",
|
||||
"consumed_qty",
|
||||
"returned_qty",
|
||||
@@ -52,7 +53,7 @@
|
||||
{
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock Uom",
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -160,18 +161,29 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:parent.reserve_stock",
|
||||
"fieldname": "stock_reserved_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Reserved Qty",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:46.680164",
|
||||
"modified": "2025-10-30 16:00:43.379828",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Order Supplied Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class SubcontractingOrderSuppliedItem(Document):
|
||||
reserve_warehouse: DF.Link | None
|
||||
returned_qty: DF.Float
|
||||
rm_item_code: DF.Link | None
|
||||
stock_reserved_qty: DF.Float
|
||||
stock_uom: DF.Link | None
|
||||
supplied_qty: DF.Float
|
||||
total_supplied_qty: DF.Float
|
||||
|
||||
@@ -164,6 +164,8 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
|
||||
for table_name in ["items", "supplied_items"]:
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
|
||||
self.update_stock_reservation_entries()
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries()
|
||||
self.repost_future_sle_and_gle()
|
||||
@@ -189,6 +191,7 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
self.set_consumed_qty_in_subcontract_order()
|
||||
self.set_subcontracting_order_status(update_bin=False)
|
||||
self.update_stock_ledger()
|
||||
self.update_stock_reservation_entries()
|
||||
self.make_gl_entries_on_cancel()
|
||||
self.repost_future_sle_and_gle()
|
||||
self.update_status()
|
||||
@@ -199,7 +202,7 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
def reset_raw_materials(self):
|
||||
self.supplied_items = []
|
||||
self.flags.reset_raw_materials = True
|
||||
self.create_raw_materials_supplied()
|
||||
self.create_raw_materials_supplied_or_received()
|
||||
|
||||
def validate_closed_subcontracting_order(self):
|
||||
for item in self.items:
|
||||
@@ -853,6 +856,17 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
if frappe.db.get_single_value("Buying Settings", "auto_create_purchase_receipt"):
|
||||
make_purchase_receipt(self, save=True, notify=True)
|
||||
|
||||
def has_reserved_stock(self):
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
get_sre_details_for_voucher,
|
||||
)
|
||||
|
||||
for item in self.supplied_items:
|
||||
if get_sre_details_for_voucher("Subcontracting Order", item.subcontracting_order):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontract_return_against_rejected_warehouse(source_name):
|
||||
|
||||
Reference in New Issue
Block a user