mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-05 06:28:29 +00:00
Merge pull request #50235 from mihir-kandoi/sre-sco
feat: stock reservation for subcontracting order
This commit is contained in:
@@ -523,6 +523,9 @@ class PurchaseOrder(BuyingController):
|
||||
if self.is_against_so():
|
||||
self.update_status_updater()
|
||||
|
||||
if self.is_against_pp():
|
||||
self.update_status_updater_if_from_pp()
|
||||
|
||||
if self.has_drop_ship_item():
|
||||
self.update_delivered_qty_in_sales_order()
|
||||
|
||||
@@ -1007,6 +1010,13 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
"Job Card", item.job_card, "wip_warehouse"
|
||||
)
|
||||
|
||||
production_plan = set([item.production_plan for item in source_doc.items if item.production_plan])
|
||||
if production_plan:
|
||||
target_doc.production_plan = production_plan.pop()
|
||||
target_doc.reserve_stock = frappe.get_single_value(
|
||||
"Stock Settings", "auto_reserve_stock"
|
||||
) or frappe.get_value("Production Plan", target_doc.production_plan, "reserve_stock")
|
||||
|
||||
if target_doc and isinstance(target_doc, str):
|
||||
target_doc = json.loads(target_doc)
|
||||
for key in ["service_items", "items", "supplied_items"]:
|
||||
|
||||
@@ -830,6 +830,7 @@
|
||||
"fieldname": "production_plan",
|
||||
"fieldtype": "Link",
|
||||
"label": "Production Plan",
|
||||
"no_copy": 1,
|
||||
"options": "Production Plan",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
@@ -948,7 +949,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-12 10:57:31.552812",
|
||||
"modified": "2025-10-30 16:51:56.761673",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
||||
@@ -1645,6 +1645,128 @@ class StockController(AccountsController):
|
||||
|
||||
gl_entries.append(self.get_gl_dict(gl_entry, item=item))
|
||||
|
||||
def update_stock_reservation_entries(self):
|
||||
def get_sre_list():
|
||||
table = frappe.qb.DocType("Stock Reservation Entry")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name)
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (table.voucher_type == data_map[purpose or self.doctype]["voucher_type"])
|
||||
& (
|
||||
table.voucher_no
|
||||
== data_map[purpose or self.doctype].get(
|
||||
"voucher_no", item.get("subcontracting_order")
|
||||
)
|
||||
)
|
||||
)
|
||||
.orderby(table.creation)
|
||||
)
|
||||
if reference_field := data_map[purpose or self.doctype].get("voucher_detail_no_field"):
|
||||
query = query.where(table.voucher_detail_no == item.get(reference_field))
|
||||
else:
|
||||
query = query.where(
|
||||
(table.item_code == item.rm_item_code) & (table.warehouse == self.supplier_warehouse)
|
||||
)
|
||||
|
||||
return query.run(pluck="name")
|
||||
|
||||
def get_data_map():
|
||||
return {
|
||||
"Subcontracting Delivery": {
|
||||
"table_name": "items",
|
||||
"voucher_type": "Subcontracting Inward Order",
|
||||
"voucher_no": self.get("subcontracting_inward_order"),
|
||||
"voucher_detail_no_field": "scio_detail",
|
||||
"field": "delivered_qty",
|
||||
},
|
||||
"Send to Subcontractor": {
|
||||
"table_name": "items",
|
||||
"voucher_type": "Subcontracting Order",
|
||||
"voucher_no": self.get("subcontracting_order"),
|
||||
"voucher_detail_no_field": "sco_rm_detail",
|
||||
"field": "transferred_qty",
|
||||
},
|
||||
"Subcontracting Receipt": {
|
||||
"table_name": "supplied_items",
|
||||
"voucher_type": "Subcontracting Order",
|
||||
"field": "consumed_qty",
|
||||
},
|
||||
}
|
||||
|
||||
purpose = self.get("purpose")
|
||||
if (
|
||||
purpose == "Subcontracting Delivery"
|
||||
or (
|
||||
purpose == "Send to Subcontractor"
|
||||
and frappe.get_value("Subcontracting Order", self.subcontracting_order, "reserve_stock")
|
||||
)
|
||||
or (self.doctype == "Subcontracting Receipt" and self.has_reserved_stock() and not self.is_return)
|
||||
):
|
||||
data_map = get_data_map()
|
||||
|
||||
field = data_map[purpose or self.doctype]["field"]
|
||||
for item in self.get(data_map[purpose or self.doctype]["table_name"]):
|
||||
sre_list = get_sre_list()
|
||||
|
||||
if not sre_list:
|
||||
continue
|
||||
|
||||
qty = item.get("transfer_qty", item.get("consumed_qty"))
|
||||
for sre in sre_list:
|
||||
if qty <= 0:
|
||||
break
|
||||
|
||||
sre_doc = frappe.get_doc("Stock Reservation Entry", sre)
|
||||
|
||||
working_qty = 0
|
||||
if sre_doc.reservation_based_on == "Serial and Batch":
|
||||
sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
|
||||
if sre_doc.has_serial_no:
|
||||
serial_nos = [d.serial_no for d in sbb.entries]
|
||||
for entry in sre_doc.sb_entries:
|
||||
if entry.serial_no in serial_nos:
|
||||
entry.delivered_qty = 1 if self._action == "submit" else 0
|
||||
entry.db_update()
|
||||
working_qty += 1
|
||||
serial_nos.remove(entry.serial_no)
|
||||
else:
|
||||
batch_qty = {d.batch_no: -1 * d.qty for d in sbb.entries}
|
||||
for entry in sre_doc.sb_entries:
|
||||
if entry.batch_no in batch_qty:
|
||||
delivered_qty = min(
|
||||
(entry.qty - entry.delivered_qty)
|
||||
if self._action == "submit"
|
||||
else entry.delivered_qty,
|
||||
batch_qty[entry.batch_no],
|
||||
)
|
||||
entry.delivered_qty += (
|
||||
delivered_qty if self._action == "submit" else (-1 * delivered_qty)
|
||||
)
|
||||
entry.db_update()
|
||||
working_qty += delivered_qty
|
||||
batch_qty[entry.batch_no] -= delivered_qty
|
||||
else:
|
||||
working_qty = min(
|
||||
(sre_doc.reserved_qty - sre_doc.get(field))
|
||||
if self._action == "submit"
|
||||
else sre_doc.get(field),
|
||||
qty,
|
||||
)
|
||||
|
||||
sre_doc.set(
|
||||
field,
|
||||
sre_doc.get(field)
|
||||
+ (working_qty if self._action == "submit" else (-1 * working_qty)),
|
||||
)
|
||||
sre_doc.db_update()
|
||||
sre_doc.update_reserved_qty_in_voucher()
|
||||
sre_doc.update_status()
|
||||
sre_doc.update_reserved_stock_in_bin()
|
||||
|
||||
qty -= working_qty
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def show_accounting_ledger_preview(company, doctype, docname):
|
||||
|
||||
@@ -497,11 +497,10 @@ class SubcontractingController(StockController):
|
||||
|
||||
if row.serial_no:
|
||||
details.serial_no.extend(get_serial_nos(row.serial_no))
|
||||
|
||||
elif row.batch_no:
|
||||
if row.batch_no:
|
||||
details.batch_no[row.batch_no] += row.qty
|
||||
|
||||
elif voucher_bundle_data:
|
||||
if not row.serial_no and not row.batch_no and voucher_bundle_data:
|
||||
bundle_key = (row.rm_item_code, row.main_item_code, row.t_warehouse, row.voucher_no)
|
||||
|
||||
bundle_data = voucher_bundle_data.get(bundle_key, frappe._dict())
|
||||
|
||||
@@ -556,131 +556,6 @@ class SubcontractingInwardController:
|
||||
item.basic_rate + (item.additional_cost / item.transfer_qty), item.precision("basic_rate")
|
||||
)
|
||||
|
||||
def update_sre_for_subcontracting_delivery(self) -> None:
|
||||
if self.purpose == "Subcontracting Delivery":
|
||||
if self._action == "submit":
|
||||
self.update_sre_for_subcontracting_delivery_submit()
|
||||
elif self._action == "cancel":
|
||||
self.update_sre_for_subcontracting_delivery_cancel()
|
||||
|
||||
def update_sre_for_subcontracting_delivery_submit(self):
|
||||
for item in self.get("items"):
|
||||
table = frappe.qb.DocType("Stock Reservation Entry")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name)
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (table.voucher_type == "Subcontracting Inward Order")
|
||||
& (table.voucher_no == self.subcontracting_inward_order)
|
||||
& (table.voucher_detail_no == item.scio_detail)
|
||||
)
|
||||
.orderby(table.creation)
|
||||
)
|
||||
sre_list = query.run(pluck="name")
|
||||
|
||||
if not sre_list:
|
||||
continue
|
||||
|
||||
qty_to_deliver = item.transfer_qty
|
||||
for sre in sre_list:
|
||||
if qty_to_deliver <= 0:
|
||||
break
|
||||
|
||||
sre_doc = frappe.get_doc("Stock Reservation Entry", sre)
|
||||
|
||||
qty_can_be_deliver = 0
|
||||
if sre_doc.reservation_based_on == "Serial and Batch":
|
||||
sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
|
||||
if sre_doc.has_serial_no:
|
||||
delivered_serial_nos = [d.serial_no for d in sbb.entries]
|
||||
for entry in sre_doc.sb_entries:
|
||||
if entry.serial_no in delivered_serial_nos:
|
||||
entry.delivered_qty = 1
|
||||
entry.db_update()
|
||||
qty_can_be_deliver += 1
|
||||
delivered_serial_nos.remove(entry.serial_no)
|
||||
else:
|
||||
delivered_batch_qty = {d.batch_no: -1 * d.qty for d in sbb.entries}
|
||||
for entry in sre_doc.sb_entries:
|
||||
if entry.batch_no in delivered_batch_qty:
|
||||
delivered_qty = min(
|
||||
(entry.qty - entry.delivered_qty),
|
||||
delivered_batch_qty[entry.batch_no],
|
||||
)
|
||||
entry.delivered_qty += delivered_qty
|
||||
entry.db_update()
|
||||
qty_can_be_deliver += delivered_qty
|
||||
delivered_batch_qty[entry.batch_no] -= delivered_qty
|
||||
else:
|
||||
qty_can_be_deliver = min((sre_doc.reserved_qty - sre_doc.delivered_qty), qty_to_deliver)
|
||||
|
||||
sre_doc.delivered_qty += qty_can_be_deliver
|
||||
sre_doc.db_update()
|
||||
sre_doc.update_status()
|
||||
sre_doc.update_reserved_stock_in_bin()
|
||||
|
||||
qty_to_deliver -= qty_can_be_deliver
|
||||
|
||||
def update_sre_for_subcontracting_delivery_cancel(self):
|
||||
for item in self.get("items"):
|
||||
table = frappe.qb.DocType("Stock Reservation Entry")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name)
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (table.voucher_type == "Subcontracting Inward Order")
|
||||
& (table.voucher_no == self.subcontracting_inward_order)
|
||||
& (table.voucher_detail_no == item.scio_detail)
|
||||
& (table.warehouse == item.s_warehouse)
|
||||
)
|
||||
.orderby(table.creation)
|
||||
)
|
||||
sre_list = query.run(pluck="name")
|
||||
|
||||
if not sre_list:
|
||||
continue
|
||||
|
||||
qty_to_undelivered = item.transfer_qty
|
||||
for sre in sre_list:
|
||||
if qty_to_undelivered <= 0:
|
||||
break
|
||||
|
||||
sre_doc = frappe.get_doc("Stock Reservation Entry", sre)
|
||||
|
||||
qty_can_be_undelivered = 0
|
||||
if sre_doc.reservation_based_on == "Serial and Batch":
|
||||
sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
|
||||
if sre_doc.has_serial_no:
|
||||
serial_nos_to_undelivered = [d.serial_no for d in sbb.entries]
|
||||
for entry in sre_doc.sb_entries:
|
||||
if entry.serial_no in serial_nos_to_undelivered:
|
||||
entry.delivered_qty = 0
|
||||
entry.db_update()
|
||||
qty_can_be_undelivered += 1
|
||||
serial_nos_to_undelivered.remove(entry.serial_no)
|
||||
else:
|
||||
batch_qty_to_undelivered = {d.batch_no: -1 * d.qty for d in sbb.entries}
|
||||
for entry in sre_doc.sb_entries:
|
||||
if entry.batch_no in batch_qty_to_undelivered:
|
||||
undelivered_qty = min(
|
||||
entry.delivered_qty, batch_qty_to_undelivered[entry.batch_no]
|
||||
)
|
||||
entry.delivered_qty -= undelivered_qty
|
||||
entry.db_update()
|
||||
qty_can_be_undelivered += undelivered_qty
|
||||
batch_qty_to_undelivered[entry.batch_no] -= undelivered_qty
|
||||
else:
|
||||
qty_can_be_undelivered = min(sre_doc.delivered_qty, qty_to_undelivered)
|
||||
|
||||
sre_doc.delivered_qty -= qty_can_be_undelivered
|
||||
sre_doc.db_update()
|
||||
sre_doc.update_status()
|
||||
sre_doc.update_reserved_stock_in_bin()
|
||||
|
||||
qty_to_undelivered -= qty_can_be_undelivered
|
||||
|
||||
def validate_receive_from_customer_cancel(self):
|
||||
if self.purpose == "Receive from Customer":
|
||||
for item in self.items:
|
||||
|
||||
@@ -1308,7 +1308,11 @@ def make_subcontracted_items():
|
||||
"Subcontracted Item SA7": {},
|
||||
"Subcontracted Item SA8": {},
|
||||
"Subcontracted Item SA9": {"stock_uom": "Litre"},
|
||||
"Subcontracted Item SA10": {},
|
||||
"Subcontracted Item SA10": {
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "SBAT.####",
|
||||
},
|
||||
}
|
||||
|
||||
for item, properties in sub_contracted_items.items():
|
||||
|
||||
@@ -8,12 +8,16 @@
|
||||
"item_code",
|
||||
"from_warehouse",
|
||||
"warehouse",
|
||||
"item_name",
|
||||
"material_request_type",
|
||||
"column_break_4",
|
||||
"item_name",
|
||||
"uom",
|
||||
"conversion_factor",
|
||||
"section_break_azee",
|
||||
"from_bom",
|
||||
"column_break_scnz",
|
||||
"main_item_code",
|
||||
"section_break_qnpt",
|
||||
"required_bom_qty",
|
||||
"projected_qty",
|
||||
"column_break_wack",
|
||||
@@ -25,6 +29,7 @@
|
||||
"min_order_qty",
|
||||
"section_break_8",
|
||||
"sales_order",
|
||||
"sub_assembly_item_reference",
|
||||
"bin_qty_section",
|
||||
"actual_qty",
|
||||
"requested_qty",
|
||||
@@ -220,12 +225,48 @@
|
||||
"label": "Stock Reserved Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "from_bom",
|
||||
"fieldname": "from_bom",
|
||||
"fieldtype": "Link",
|
||||
"label": "From BOM",
|
||||
"mandatory_depends_on": "eval:parent.reserve_stock",
|
||||
"no_copy": 1,
|
||||
"options": "BOM",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sub_assembly_item_reference",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Sub Assembly Item Reference",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_qnpt",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "main_item_code",
|
||||
"fieldname": "main_item_code",
|
||||
"fieldtype": "Link",
|
||||
"label": "Main Item Code",
|
||||
"mandatory_depends_on": "eval:parent.reserve_stock",
|
||||
"no_copy": 1,
|
||||
"options": "Item",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_scnz",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-05-01 14:50:55.805442",
|
||||
"modified": "2025-10-30 17:01:25.996352",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Material Request Plan Item",
|
||||
|
||||
@@ -17,9 +17,11 @@ class MaterialRequestPlanItem(Document):
|
||||
actual_qty: DF.Float
|
||||
conversion_factor: DF.Float
|
||||
description: DF.TextEditor | None
|
||||
from_bom: DF.Link | None
|
||||
from_warehouse: DF.Link | None
|
||||
item_code: DF.Link
|
||||
item_name: DF.Data | None
|
||||
main_item_code: DF.Link | None
|
||||
material_request_type: DF.Literal[
|
||||
"",
|
||||
"Purchase",
|
||||
@@ -43,6 +45,7 @@ class MaterialRequestPlanItem(Document):
|
||||
sales_order: DF.Link | None
|
||||
schedule_date: DF.Date | None
|
||||
stock_reserved_qty: DF.Float
|
||||
sub_assembly_item_reference: DF.Data | None
|
||||
uom: DF.Link | None
|
||||
warehouse: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -568,6 +568,7 @@ class ProductionPlan(Document):
|
||||
def on_submit(self):
|
||||
self.update_bin_qty()
|
||||
self.update_sales_order()
|
||||
self.add_reference_to_raw_materials()
|
||||
self.update_stock_reservation()
|
||||
|
||||
def on_cancel(self):
|
||||
@@ -583,6 +584,24 @@ class ProductionPlan(Document):
|
||||
|
||||
make_stock_reservation_entries(self)
|
||||
|
||||
def add_reference_to_raw_materials(self):
|
||||
for item in self.mr_items:
|
||||
if reference := next(
|
||||
(
|
||||
sa_item.name
|
||||
for sa_item in self.sub_assembly_items
|
||||
if sa_item.production_item == item.main_item_code and sa_item.bom_no == item.from_bom
|
||||
),
|
||||
None,
|
||||
):
|
||||
item.db_set("sub_assembly_item_reference", reference)
|
||||
elif self.reserve_stock and item.main_item_code and item.from_bom:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Sub assembly item references are missing. Please fetch the sub assemblies and raw materials again."
|
||||
)
|
||||
)
|
||||
|
||||
def update_sales_order(self):
|
||||
sales_orders = [row.sales_order for row in self.po_items if row.sales_order]
|
||||
if sales_orders:
|
||||
@@ -1382,14 +1401,14 @@ def get_material_request_items(
|
||||
include_safety_stock,
|
||||
warehouse,
|
||||
bin_dict,
|
||||
total_qty,
|
||||
):
|
||||
total_qty = row["qty"]
|
||||
|
||||
required_qty = 0
|
||||
if not ignore_existing_ordered_qty or bin_dict.get("projected_qty", 0) < 0:
|
||||
required_qty = total_qty
|
||||
elif total_qty > bin_dict.get("projected_qty", 0):
|
||||
required_qty = total_qty - bin_dict.get("projected_qty", 0)
|
||||
required_qty = total_qty[row.get("item_code")]
|
||||
elif total_qty[row.get("item_code")] > bin_dict.get("projected_qty", 0):
|
||||
required_qty = total_qty[row.get("item_code")] - bin_dict.get("projected_qty", 0)
|
||||
total_qty[row.get("item_code")] -= required_qty
|
||||
|
||||
if doc.get("consider_minimum_order_qty") and required_qty > 0 and required_qty < row["min_order_qty"]:
|
||||
required_qty = row["min_order_qty"]
|
||||
@@ -1432,7 +1451,7 @@ def get_material_request_items(
|
||||
"item_name": row.item_name,
|
||||
"quantity": required_qty / conversion_factor,
|
||||
"conversion_factor": conversion_factor,
|
||||
"required_bom_qty": total_qty,
|
||||
"required_bom_qty": row.get("qty"),
|
||||
"stock_uom": row.get("stock_uom"),
|
||||
"warehouse": warehouse
|
||||
or row.get("source_warehouse")
|
||||
@@ -1448,7 +1467,8 @@ def get_material_request_items(
|
||||
"sales_order": sales_order,
|
||||
"description": row.get("description"),
|
||||
"uom": row.get("purchase_uom") or row.get("stock_uom"),
|
||||
"main_bom_item": row.get("main_bom_item"),
|
||||
"main_item_code": row.get("main_bom_item"),
|
||||
"from_bom": row.get("main_bom"),
|
||||
}
|
||||
|
||||
|
||||
@@ -1629,7 +1649,10 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
sub_assembly_items = defaultdict(int)
|
||||
if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"):
|
||||
for d in doc.get("sub_assembly_items"):
|
||||
sub_assembly_items[(d.get("production_item"), d.get("bom_no"))] += d.get("qty")
|
||||
sub_assembly_items[
|
||||
(d.get("production_item"), d.get("bom_no"), d.get("type_of_manufacturing"))
|
||||
] += d.get("qty")
|
||||
sub_assembly_items = {k[:2]: v for k, v in sub_assembly_items.items()}
|
||||
|
||||
for data in po_items:
|
||||
if not data.get("include_exploded_items") and doc.get("sub_assembly_items"):
|
||||
@@ -1718,19 +1741,21 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
|
||||
sales_order = data.get("sales_order")
|
||||
|
||||
for item_code, details in item_details.items():
|
||||
for key, details in item_details.items():
|
||||
so_item_details.setdefault(sales_order, frappe._dict())
|
||||
if item_code in so_item_details.get(sales_order, {}):
|
||||
so_item_details[sales_order][item_code]["qty"] = so_item_details[sales_order][item_code].get(
|
||||
if key in so_item_details.get(sales_order, {}):
|
||||
so_item_details[sales_order][key]["qty"] = so_item_details[sales_order][key].get(
|
||||
"qty", 0
|
||||
) + flt(details.qty)
|
||||
else:
|
||||
so_item_details[sales_order][item_code] = details
|
||||
so_item_details[sales_order][key] = details
|
||||
|
||||
mr_items = []
|
||||
for sales_order in so_item_details:
|
||||
item_dict = so_item_details[sales_order]
|
||||
total_qty = defaultdict(float)
|
||||
for details in item_dict.values():
|
||||
total_qty[details.item_code] += flt(details.qty)
|
||||
bin_dict = get_bin_details(details, doc.company, warehouse)
|
||||
bin_dict = bin_dict[0] if bin_dict else {}
|
||||
|
||||
@@ -1744,6 +1769,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
include_safety_stock,
|
||||
warehouse,
|
||||
bin_dict,
|
||||
total_qty,
|
||||
)
|
||||
if items:
|
||||
mr_items.append(items)
|
||||
@@ -1998,7 +2024,7 @@ def get_raw_materials_of_sub_assembly_items(
|
||||
item_default = frappe.qb.DocType("Item Default")
|
||||
item_uom = frappe.qb.DocType("UOM Conversion Detail")
|
||||
|
||||
items = (
|
||||
query = (
|
||||
frappe.qb.from_(bei)
|
||||
.join(bom)
|
||||
.on(bom.name == bei.parent)
|
||||
@@ -2024,6 +2050,7 @@ def get_raw_materials_of_sub_assembly_items(
|
||||
item_uom.conversion_factor,
|
||||
item.safety_stock,
|
||||
bom.item.as_("main_bom_item"),
|
||||
bom.name.as_("main_bom"),
|
||||
)
|
||||
.where(
|
||||
(bei.docstatus == 1)
|
||||
@@ -2032,11 +2059,13 @@ def get_raw_materials_of_sub_assembly_items(
|
||||
& (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1)
|
||||
)
|
||||
.groupby(bei.item_code, bei.stock_uom)
|
||||
).run(as_dict=True)
|
||||
)
|
||||
|
||||
for item in items:
|
||||
for item in query.run(as_dict=True):
|
||||
key = (item.item_code, item.bom_no)
|
||||
if (item.bom_no and key not in sub_assembly_items) or (item.item_code in existing_sub_assembly_items):
|
||||
if (item.bom_no and key not in sub_assembly_items) or (
|
||||
(item.item_code, item.bom_no or item.main_bom) in existing_sub_assembly_items
|
||||
):
|
||||
continue
|
||||
|
||||
if item.bom_no:
|
||||
@@ -2050,15 +2079,15 @@ def get_raw_materials_of_sub_assembly_items(
|
||||
sub_assembly_items,
|
||||
planned_qty=planned_qty,
|
||||
)
|
||||
existing_sub_assembly_items.add(item.item_code)
|
||||
existing_sub_assembly_items.add((item.item_code, item.bom_no or item.main_bom))
|
||||
else:
|
||||
if not item.conversion_factor and item.purchase_uom:
|
||||
item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom)
|
||||
|
||||
if details := item_details.get(item.get("item_code")):
|
||||
if details := item_details.get((item.get("item_code"), item.get("main_bom"))):
|
||||
details.qty += item.get("qty")
|
||||
else:
|
||||
item_details.setdefault(item.get("item_code"), item)
|
||||
item_details.setdefault((item.get("item_code"), item.get("main_bom")), item)
|
||||
|
||||
return item_details
|
||||
|
||||
|
||||
@@ -1944,11 +1944,17 @@ class TestProductionPlan(IntegrationTestCase):
|
||||
|
||||
mr_items = get_items_for_material_requests(plan.as_dict())
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
mr_items_dict = defaultdict(float)
|
||||
for item in mr_items:
|
||||
mr_items_dict[item.get("item_code")] += item.get("quantity")
|
||||
|
||||
# RM Item 1 (FG1 (100 + 100) + FG2 (50) + FG3 (10) - 90 in stock - 80 sub assembly stock)
|
||||
self.assertEqual(mr_items[0].get("quantity"), 90)
|
||||
self.assertEqual(mr_items_dict["RM Item 1"], 90)
|
||||
|
||||
# RM Item 2 (FG1 (100) + FG2 (50) + FG4 (10) - 80 sub assembly stock)
|
||||
self.assertEqual(mr_items[1].get("quantity"), 80)
|
||||
self.assertEqual(mr_items_dict["RM Item 2"], 80)
|
||||
|
||||
def test_stock_reservation_against_production_plan(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
@@ -2364,9 +2370,6 @@ class TestProductionPlan(IntegrationTestCase):
|
||||
def test_production_plan_for_partial_sub_assembly_items(self):
|
||||
from erpnext.controllers.status_updater import OverAllowanceError
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
from erpnext.subcontracting.doctype.subcontracting_bom.test_subcontracting_bom import (
|
||||
create_subcontracting_bom,
|
||||
)
|
||||
|
||||
frappe.flags.test_print = False
|
||||
|
||||
@@ -2440,6 +2443,7 @@ def create_production_plan(**args):
|
||||
"skip_available_sub_assembly_item": args.skip_available_sub_assembly_item or 0,
|
||||
"sub_assembly_warehouse": args.sub_assembly_warehouse,
|
||||
"reserve_stock": args.reserve_stock or 0,
|
||||
"for_warehouse": args.for_warehouse or None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -79,13 +79,14 @@
|
||||
"fieldname": "received_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Received Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "bom_no",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Bom No",
|
||||
"label": "BOM No",
|
||||
"options": "BOM"
|
||||
},
|
||||
{
|
||||
@@ -245,7 +246,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-10 13:36:24.759101",
|
||||
"modified": "2025-11-03 14:33:50.677717",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Sub Assembly Item",
|
||||
|
||||
@@ -2502,18 +2502,11 @@ def get_auto_batch_nos(kwargs):
|
||||
|
||||
|
||||
def get_batch_nos_from_sre(kwargs):
|
||||
from frappe.query_builder.functions import Max, Min, Sum
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Stock Reservation Entry")
|
||||
child_table = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
||||
if kwargs.based_on == "LIFO":
|
||||
creation_field = Max(child_table.creation).as_("sort_creation")
|
||||
order = frappe.query_builder.Order.desc
|
||||
else:
|
||||
creation_field = Min(child_table.creation).as_("sort_creation")
|
||||
order = frappe.query_builder.Order.asc
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.join(child_table)
|
||||
@@ -2522,7 +2515,6 @@ def get_batch_nos_from_sre(kwargs):
|
||||
child_table.batch_no,
|
||||
child_table.warehouse,
|
||||
Sum(child_table.qty - child_table.delivered_qty).as_("qty"),
|
||||
creation_field,
|
||||
)
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
@@ -2530,7 +2522,6 @@ def get_batch_nos_from_sre(kwargs):
|
||||
& (child_table.qty != child_table.delivered_qty)
|
||||
)
|
||||
.groupby(child_table.batch_no, child_table.warehouse)
|
||||
.orderby("sort_creation", order=order)
|
||||
.orderby(child_table.batch_no, order=frappe.query_builder.Order.asc)
|
||||
)
|
||||
|
||||
|
||||
@@ -202,6 +202,12 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
for item in self.get("items"):
|
||||
item.update(get_bin_details(item.item_code, item.s_warehouse))
|
||||
|
||||
def before_insert(self):
|
||||
if self.subcontracting_order and frappe.get_cached_value(
|
||||
"Subcontracting Order", self.subcontracting_order, "reserve_stock"
|
||||
):
|
||||
self.set_serial_batch_from_reserved_entry()
|
||||
|
||||
def before_validate(self):
|
||||
from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule
|
||||
|
||||
@@ -274,9 +280,10 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
self.update_work_order()
|
||||
self.update_disassembled_order()
|
||||
self.adjust_stock_reservation_entries_for_return()
|
||||
self.update_sre_for_subcontracting_delivery()
|
||||
self.update_stock_reservation_entries()
|
||||
self.update_stock_ledger()
|
||||
self.make_stock_reserve_for_wip_and_fg()
|
||||
self.reserve_stock_for_subcontracting()
|
||||
|
||||
self.update_subcontract_order_supplied_items()
|
||||
self.update_subcontracting_order_status()
|
||||
@@ -324,7 +331,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
self.update_transferred_qty()
|
||||
self.update_quality_inspection()
|
||||
self.adjust_stock_reservation_entries_for_return()
|
||||
self.update_sre_for_subcontracting_delivery()
|
||||
self.update_stock_reservation_entries()
|
||||
self.delete_auto_created_batches()
|
||||
self.delete_linked_stock_entry()
|
||||
|
||||
@@ -1889,6 +1896,30 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
|
||||
pro_doc.set_reserved_qty_for_wip_and_fg(self)
|
||||
|
||||
def reserve_stock_for_subcontracting(self):
|
||||
if self.purpose == "Send to Subcontractor" and frappe.get_value(
|
||||
"Subcontracting Order", self.subcontracting_order, "reserve_stock"
|
||||
):
|
||||
items = {}
|
||||
for item in self.items:
|
||||
if item.sco_rm_detail in items:
|
||||
items[item.sco_rm_detail].qty_to_reserve += item.transfer_qty
|
||||
items[item.sco_rm_detail].serial_and_batch_bundles.append(item.serial_and_batch_bundle)
|
||||
else:
|
||||
items[item.sco_rm_detail] = frappe._dict(
|
||||
{
|
||||
"name": item.sco_rm_detail,
|
||||
"qty_to_reserve": item.transfer_qty,
|
||||
"warehouse": item.t_warehouse,
|
||||
"reference_voucher_detail_no": item.name,
|
||||
"serial_and_batch_bundles": [item.serial_and_batch_bundle],
|
||||
}
|
||||
)
|
||||
|
||||
frappe.get_doc("Subcontracting Order", self.subcontracting_order).reserve_raw_materials(
|
||||
items=items.values(), stock_entry=self.name
|
||||
)
|
||||
|
||||
def cancel_stock_reserve_for_wip_and_fg(self):
|
||||
if self.is_stock_reserve_for_work_order():
|
||||
pro_doc = frappe.get_doc("Work Order", self.work_order)
|
||||
@@ -2230,21 +2261,16 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
self.calculate_rate_and_amount(raise_error_if_no_rate=False)
|
||||
|
||||
def set_serial_batch_from_reserved_entry(self):
|
||||
if not self.work_order:
|
||||
return
|
||||
if self.work_order and frappe.get_cached_value("Work Order", self.work_order, "reserve_stock"):
|
||||
skip_transfer = frappe.get_cached_value("Work Order", self.work_order, "skip_transfer")
|
||||
|
||||
if not frappe.get_cached_value("Work Order", self.work_order, "reserve_stock"):
|
||||
return
|
||||
|
||||
skip_transfer = frappe.get_cached_value("Work Order", self.work_order, "skip_transfer")
|
||||
|
||||
if (
|
||||
self.purpose not in ["Material Transfer for Manufacture"]
|
||||
and frappe.db.get_single_value("Manufacturing Settings", "backflush_raw_materials_based_on")
|
||||
!= "BOM"
|
||||
and not skip_transfer
|
||||
):
|
||||
return
|
||||
if (
|
||||
self.purpose not in ["Material Transfer for Manufacture"]
|
||||
and frappe.db.get_single_value("Manufacturing Settings", "backflush_raw_materials_based_on")
|
||||
!= "BOM"
|
||||
and not skip_transfer
|
||||
):
|
||||
return
|
||||
|
||||
reservation_entries = self.get_available_reserved_materials()
|
||||
if not reservation_entries:
|
||||
@@ -2252,6 +2278,9 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
|
||||
new_items_to_add = []
|
||||
for d in self.items:
|
||||
if d.serial_and_batch_bundle or d.serial_no or d.batch_no:
|
||||
continue
|
||||
|
||||
key = (d.item_code, d.s_warehouse)
|
||||
if details := reservation_entries.get(key):
|
||||
original_qty = d.qty
|
||||
@@ -2363,7 +2392,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
)
|
||||
.where(
|
||||
(doctype.docstatus == 1)
|
||||
& (doctype.voucher_no == self.work_order)
|
||||
& (doctype.voucher_no == (self.work_order or self.subcontracting_order))
|
||||
& (serial_batch_doc.delivered_qty < serial_batch_doc.qty)
|
||||
)
|
||||
.orderby(serial_batch_doc.idx)
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "voucher_type",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "\nSales Order\nWork Order\nSubcontracting Inward Order\nProduction Plan",
|
||||
"options": "\nSales Order\nWork Order\nSubcontracting Inward Order\nProduction Plan\nSubcontracting Order",
|
||||
"print_width": "150px",
|
||||
"read_only": 1,
|
||||
"width": "150px"
|
||||
@@ -315,8 +315,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "production_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Production"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_qdwj",
|
||||
@@ -335,7 +334,7 @@
|
||||
{
|
||||
"fieldname": "transferred_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Qty in WIP Warehouse"
|
||||
"label": "Transferred Qty"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -344,7 +343,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-12 19:48:33.170835",
|
||||
"modified": "2025-11-10 16:09:10.380024",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reservation Entry",
|
||||
|
||||
@@ -64,7 +64,12 @@ class StockReservationEntry(Document):
|
||||
voucher_no: DF.DynamicLink | None
|
||||
voucher_qty: DF.Float
|
||||
voucher_type: DF.Literal[
|
||||
"", "Sales Order", "Work Order", "Subcontracting Inward Order", "Production Plan"
|
||||
"",
|
||||
"Sales Order",
|
||||
"Work Order",
|
||||
"Subcontracting Inward Order",
|
||||
"Production Plan",
|
||||
"Subcontracting Order",
|
||||
]
|
||||
warehouse: DF.Link | None
|
||||
# end: auto-generated types
|
||||
@@ -338,7 +343,7 @@ class StockReservationEntry(Document):
|
||||
|
||||
def validate_reservation_based_on_serial_and_batch(self) -> None:
|
||||
"""Validates `Reserved Qty`, `Serial and Batch Nos` when `Reservation Based On` is `Serial and Batch`."""
|
||||
if self.voucher_type == "Work Order":
|
||||
if self.voucher_type in ["Work Order", "Subcontracting Order"]:
|
||||
return
|
||||
|
||||
if self.reservation_based_on == "Serial and Batch":
|
||||
@@ -460,13 +465,14 @@ class StockReservationEntry(Document):
|
||||
"Sales Order": "Sales Order Item",
|
||||
"Work Order": "Work Order Item",
|
||||
"Production Plan": "Production Plan Sub Assembly Item",
|
||||
"Subcontracting Order": "Subcontracting Order Supplied Item",
|
||||
}.get(self.voucher_type, None)
|
||||
|
||||
if item_doctype:
|
||||
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||
reserved_qty = (
|
||||
frappe.qb.from_(sre)
|
||||
.select(Sum(sre.reserved_qty))
|
||||
.select(Sum(sre.reserved_qty - sre.delivered_qty - sre.transferred_qty - sre.consumed_qty))
|
||||
.where(
|
||||
(sre.docstatus == 1)
|
||||
& (sre.voucher_type == self.voucher_type)
|
||||
@@ -574,7 +580,7 @@ class StockReservationEntry(Document):
|
||||
)
|
||||
|
||||
from_voucher_detail_no = None
|
||||
if self.from_voucher_type and self.from_voucher_type == "Stock Entry":
|
||||
if self.from_voucher_type and self.from_voucher_type in ["Stock Entry", "Production Plan"]:
|
||||
from_voucher_detail_no = self.from_voucher_detail_no
|
||||
|
||||
total_reserved_qty = get_sre_reserved_qty_for_voucher_detail_no(
|
||||
@@ -1276,7 +1282,7 @@ class StockReservation:
|
||||
if not reservation_entries:
|
||||
return
|
||||
|
||||
entries_to_reserve = frappe._dict({})
|
||||
entries_to_reserve = frappe._dict()
|
||||
for row in reservation_entries:
|
||||
reserved_qty_field = "reserved_qty" if row.reservation_based_on == "Qty" else "sabb_qty"
|
||||
delivered_qty_field = (
|
||||
@@ -1293,7 +1299,7 @@ class StockReservation:
|
||||
if available_qty <= 0:
|
||||
continue
|
||||
|
||||
key = (row.item_code, row.warehouse)
|
||||
key = (row.item_code, row.warehouse, entry.voucher_detail_no)
|
||||
|
||||
if key not in entries_to_reserve:
|
||||
entries_to_reserve.setdefault(
|
||||
@@ -1303,7 +1309,7 @@ class StockReservation:
|
||||
"qty_to_reserve": 0.0,
|
||||
"item_code": row.item_code,
|
||||
"warehouse": row.warehouse,
|
||||
"voucher_type": entry.voucher_type,
|
||||
"voucher_type": entry.voucher_type or to_doctype,
|
||||
"voucher_no": entry.voucher_no,
|
||||
"voucher_detail_no": entry.voucher_detail_no,
|
||||
"serial_nos": [],
|
||||
@@ -1475,6 +1481,9 @@ class StockReservation:
|
||||
.orderby(sabb_entry.idx)
|
||||
)
|
||||
|
||||
if self.items and (data := [item.from_voucher_detail_no for item in self.items]):
|
||||
query = query.where(sre.voucher_detail_no.isin(data))
|
||||
|
||||
if against_fg_item:
|
||||
query = query.where(
|
||||
sre.voucher_detail_no.isin(
|
||||
@@ -1490,9 +1499,14 @@ class StockReservation:
|
||||
|
||||
def get_items_to_reserve(self, docnames, from_doctype, to_doctype):
|
||||
field = frappe.scrub(from_doctype)
|
||||
item_code_fieldname, child_table_suffix = (
|
||||
("rm_item_code", " Supplied Item")
|
||||
if to_doctype == "Subcontracting Order"
|
||||
else ("item_code", " Item")
|
||||
)
|
||||
|
||||
doctype = frappe.qb.DocType(to_doctype)
|
||||
child_doctype = frappe.qb.DocType(to_doctype + " Item")
|
||||
child_doctype = frappe.qb.DocType(to_doctype + child_table_suffix)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(doctype)
|
||||
@@ -1501,11 +1515,12 @@ class StockReservation:
|
||||
.select(
|
||||
doctype.name.as_("voucher_no"),
|
||||
child_doctype.name.as_("voucher_detail_no"),
|
||||
child_doctype.item_code,
|
||||
child_doctype[item_code_fieldname].as_("item_code"),
|
||||
doctype.company,
|
||||
child_doctype.stock_uom,
|
||||
)
|
||||
.where((doctype.docstatus == 1) & (doctype[field].isin(docnames)))
|
||||
.groupby(child_doctype.name)
|
||||
)
|
||||
|
||||
if to_doctype == "Work Order":
|
||||
@@ -1523,6 +1538,15 @@ class StockReservation:
|
||||
(doctype.qty > doctype.material_transferred_for_manufacturing)
|
||||
& (doctype.status != "Completed")
|
||||
)
|
||||
elif to_doctype == "Subcontracting Order":
|
||||
query = query.select(
|
||||
child_doctype.stock_reserved_qty,
|
||||
child_doctype.required_qty.as_("qty"),
|
||||
child_doctype.reserve_warehouse.as_("source_warehouse"),
|
||||
)
|
||||
|
||||
if self.items and (data := [item.voucher_detail_no for item in self.items]):
|
||||
query = query.where(child_doctype.name.isin(data))
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
items = []
|
||||
|
||||
@@ -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