mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-03 12:19:12 +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():
|
if self.is_against_so():
|
||||||
self.update_status_updater()
|
self.update_status_updater()
|
||||||
|
|
||||||
|
if self.is_against_pp():
|
||||||
|
self.update_status_updater_if_from_pp()
|
||||||
|
|
||||||
if self.has_drop_ship_item():
|
if self.has_drop_ship_item():
|
||||||
self.update_delivered_qty_in_sales_order()
|
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"
|
"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):
|
if target_doc and isinstance(target_doc, str):
|
||||||
target_doc = json.loads(target_doc)
|
target_doc = json.loads(target_doc)
|
||||||
for key in ["service_items", "items", "supplied_items"]:
|
for key in ["service_items", "items", "supplied_items"]:
|
||||||
|
|||||||
@@ -830,6 +830,7 @@
|
|||||||
"fieldname": "production_plan",
|
"fieldname": "production_plan",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Production Plan",
|
"label": "Production Plan",
|
||||||
|
"no_copy": 1,
|
||||||
"options": "Production Plan",
|
"options": "Production Plan",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
@@ -948,7 +949,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-12 10:57:31.552812",
|
"modified": "2025-10-30 16:51:56.761673",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order Item",
|
"name": "Purchase Order Item",
|
||||||
|
|||||||
@@ -1645,6 +1645,128 @@ class StockController(AccountsController):
|
|||||||
|
|
||||||
gl_entries.append(self.get_gl_dict(gl_entry, item=item))
|
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()
|
@frappe.whitelist()
|
||||||
def show_accounting_ledger_preview(company, doctype, docname):
|
def show_accounting_ledger_preview(company, doctype, docname):
|
||||||
|
|||||||
@@ -497,11 +497,10 @@ class SubcontractingController(StockController):
|
|||||||
|
|
||||||
if row.serial_no:
|
if row.serial_no:
|
||||||
details.serial_no.extend(get_serial_nos(row.serial_no))
|
details.serial_no.extend(get_serial_nos(row.serial_no))
|
||||||
|
if row.batch_no:
|
||||||
elif row.batch_no:
|
|
||||||
details.batch_no[row.batch_no] += row.qty
|
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_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())
|
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")
|
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):
|
def validate_receive_from_customer_cancel(self):
|
||||||
if self.purpose == "Receive from Customer":
|
if self.purpose == "Receive from Customer":
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
|
|||||||
@@ -1308,7 +1308,11 @@ def make_subcontracted_items():
|
|||||||
"Subcontracted Item SA7": {},
|
"Subcontracted Item SA7": {},
|
||||||
"Subcontracted Item SA8": {},
|
"Subcontracted Item SA8": {},
|
||||||
"Subcontracted Item SA9": {"stock_uom": "Litre"},
|
"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():
|
for item, properties in sub_contracted_items.items():
|
||||||
|
|||||||
@@ -8,12 +8,16 @@
|
|||||||
"item_code",
|
"item_code",
|
||||||
"from_warehouse",
|
"from_warehouse",
|
||||||
"warehouse",
|
"warehouse",
|
||||||
"item_name",
|
|
||||||
"material_request_type",
|
"material_request_type",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
|
"item_name",
|
||||||
"uom",
|
"uom",
|
||||||
"conversion_factor",
|
"conversion_factor",
|
||||||
"section_break_azee",
|
"section_break_azee",
|
||||||
|
"from_bom",
|
||||||
|
"column_break_scnz",
|
||||||
|
"main_item_code",
|
||||||
|
"section_break_qnpt",
|
||||||
"required_bom_qty",
|
"required_bom_qty",
|
||||||
"projected_qty",
|
"projected_qty",
|
||||||
"column_break_wack",
|
"column_break_wack",
|
||||||
@@ -25,6 +29,7 @@
|
|||||||
"min_order_qty",
|
"min_order_qty",
|
||||||
"section_break_8",
|
"section_break_8",
|
||||||
"sales_order",
|
"sales_order",
|
||||||
|
"sub_assembly_item_reference",
|
||||||
"bin_qty_section",
|
"bin_qty_section",
|
||||||
"actual_qty",
|
"actual_qty",
|
||||||
"requested_qty",
|
"requested_qty",
|
||||||
@@ -220,12 +225,48 @@
|
|||||||
"label": "Stock Reserved Qty",
|
"label": "Stock Reserved Qty",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"read_only": 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,
|
"grid_page_length": 50,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-05-01 14:50:55.805442",
|
"modified": "2025-10-30 17:01:25.996352",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Material Request Plan Item",
|
"name": "Material Request Plan Item",
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ class MaterialRequestPlanItem(Document):
|
|||||||
actual_qty: DF.Float
|
actual_qty: DF.Float
|
||||||
conversion_factor: DF.Float
|
conversion_factor: DF.Float
|
||||||
description: DF.TextEditor | None
|
description: DF.TextEditor | None
|
||||||
|
from_bom: DF.Link | None
|
||||||
from_warehouse: DF.Link | None
|
from_warehouse: DF.Link | None
|
||||||
item_code: DF.Link
|
item_code: DF.Link
|
||||||
item_name: DF.Data | None
|
item_name: DF.Data | None
|
||||||
|
main_item_code: DF.Link | None
|
||||||
material_request_type: DF.Literal[
|
material_request_type: DF.Literal[
|
||||||
"",
|
"",
|
||||||
"Purchase",
|
"Purchase",
|
||||||
@@ -43,6 +45,7 @@ class MaterialRequestPlanItem(Document):
|
|||||||
sales_order: DF.Link | None
|
sales_order: DF.Link | None
|
||||||
schedule_date: DF.Date | None
|
schedule_date: DF.Date | None
|
||||||
stock_reserved_qty: DF.Float
|
stock_reserved_qty: DF.Float
|
||||||
|
sub_assembly_item_reference: DF.Data | None
|
||||||
uom: DF.Link | None
|
uom: DF.Link | None
|
||||||
warehouse: DF.Link
|
warehouse: DF.Link
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|||||||
@@ -568,6 +568,7 @@ class ProductionPlan(Document):
|
|||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.update_bin_qty()
|
self.update_bin_qty()
|
||||||
self.update_sales_order()
|
self.update_sales_order()
|
||||||
|
self.add_reference_to_raw_materials()
|
||||||
self.update_stock_reservation()
|
self.update_stock_reservation()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
@@ -583,6 +584,24 @@ class ProductionPlan(Document):
|
|||||||
|
|
||||||
make_stock_reservation_entries(self)
|
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):
|
def update_sales_order(self):
|
||||||
sales_orders = [row.sales_order for row in self.po_items if row.sales_order]
|
sales_orders = [row.sales_order for row in self.po_items if row.sales_order]
|
||||||
if sales_orders:
|
if sales_orders:
|
||||||
@@ -1382,14 +1401,14 @@ def get_material_request_items(
|
|||||||
include_safety_stock,
|
include_safety_stock,
|
||||||
warehouse,
|
warehouse,
|
||||||
bin_dict,
|
bin_dict,
|
||||||
|
total_qty,
|
||||||
):
|
):
|
||||||
total_qty = row["qty"]
|
|
||||||
|
|
||||||
required_qty = 0
|
required_qty = 0
|
||||||
if not ignore_existing_ordered_qty or bin_dict.get("projected_qty", 0) < 0:
|
if not ignore_existing_ordered_qty or bin_dict.get("projected_qty", 0) < 0:
|
||||||
required_qty = total_qty
|
required_qty = total_qty[row.get("item_code")]
|
||||||
elif total_qty > bin_dict.get("projected_qty", 0):
|
elif total_qty[row.get("item_code")] > bin_dict.get("projected_qty", 0):
|
||||||
required_qty = total_qty - 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"]:
|
if doc.get("consider_minimum_order_qty") and required_qty > 0 and required_qty < row["min_order_qty"]:
|
||||||
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,
|
"item_name": row.item_name,
|
||||||
"quantity": required_qty / conversion_factor,
|
"quantity": required_qty / conversion_factor,
|
||||||
"conversion_factor": conversion_factor,
|
"conversion_factor": conversion_factor,
|
||||||
"required_bom_qty": total_qty,
|
"required_bom_qty": row.get("qty"),
|
||||||
"stock_uom": row.get("stock_uom"),
|
"stock_uom": row.get("stock_uom"),
|
||||||
"warehouse": warehouse
|
"warehouse": warehouse
|
||||||
or row.get("source_warehouse")
|
or row.get("source_warehouse")
|
||||||
@@ -1448,7 +1467,8 @@ def get_material_request_items(
|
|||||||
"sales_order": sales_order,
|
"sales_order": sales_order,
|
||||||
"description": row.get("description"),
|
"description": row.get("description"),
|
||||||
"uom": row.get("purchase_uom") or row.get("stock_uom"),
|
"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)
|
sub_assembly_items = defaultdict(int)
|
||||||
if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"):
|
if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"):
|
||||||
for d in 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:
|
for data in po_items:
|
||||||
if not data.get("include_exploded_items") and doc.get("sub_assembly_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")
|
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())
|
so_item_details.setdefault(sales_order, frappe._dict())
|
||||||
if item_code in so_item_details.get(sales_order, {}):
|
if key in so_item_details.get(sales_order, {}):
|
||||||
so_item_details[sales_order][item_code]["qty"] = so_item_details[sales_order][item_code].get(
|
so_item_details[sales_order][key]["qty"] = so_item_details[sales_order][key].get(
|
||||||
"qty", 0
|
"qty", 0
|
||||||
) + flt(details.qty)
|
) + flt(details.qty)
|
||||||
else:
|
else:
|
||||||
so_item_details[sales_order][item_code] = details
|
so_item_details[sales_order][key] = details
|
||||||
|
|
||||||
mr_items = []
|
mr_items = []
|
||||||
for sales_order in so_item_details:
|
for sales_order in so_item_details:
|
||||||
item_dict = so_item_details[sales_order]
|
item_dict = so_item_details[sales_order]
|
||||||
|
total_qty = defaultdict(float)
|
||||||
for details in item_dict.values():
|
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 = get_bin_details(details, doc.company, warehouse)
|
||||||
bin_dict = bin_dict[0] if bin_dict else {}
|
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,
|
include_safety_stock,
|
||||||
warehouse,
|
warehouse,
|
||||||
bin_dict,
|
bin_dict,
|
||||||
|
total_qty,
|
||||||
)
|
)
|
||||||
if items:
|
if items:
|
||||||
mr_items.append(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_default = frappe.qb.DocType("Item Default")
|
||||||
item_uom = frappe.qb.DocType("UOM Conversion Detail")
|
item_uom = frappe.qb.DocType("UOM Conversion Detail")
|
||||||
|
|
||||||
items = (
|
query = (
|
||||||
frappe.qb.from_(bei)
|
frappe.qb.from_(bei)
|
||||||
.join(bom)
|
.join(bom)
|
||||||
.on(bom.name == bei.parent)
|
.on(bom.name == bei.parent)
|
||||||
@@ -2024,6 +2050,7 @@ def get_raw_materials_of_sub_assembly_items(
|
|||||||
item_uom.conversion_factor,
|
item_uom.conversion_factor,
|
||||||
item.safety_stock,
|
item.safety_stock,
|
||||||
bom.item.as_("main_bom_item"),
|
bom.item.as_("main_bom_item"),
|
||||||
|
bom.name.as_("main_bom"),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(bei.docstatus == 1)
|
(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)
|
& (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)
|
.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)
|
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
|
continue
|
||||||
|
|
||||||
if item.bom_no:
|
if item.bom_no:
|
||||||
@@ -2050,15 +2079,15 @@ def get_raw_materials_of_sub_assembly_items(
|
|||||||
sub_assembly_items,
|
sub_assembly_items,
|
||||||
planned_qty=planned_qty,
|
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:
|
else:
|
||||||
if not item.conversion_factor and item.purchase_uom:
|
if not item.conversion_factor and item.purchase_uom:
|
||||||
item.conversion_factor = get_uom_conversion_factor(item.item_code, 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")
|
details.qty += item.get("qty")
|
||||||
else:
|
else:
|
||||||
item_details.setdefault(item.get("item_code"), item)
|
item_details.setdefault((item.get("item_code"), item.get("main_bom")), item)
|
||||||
|
|
||||||
return item_details
|
return item_details
|
||||||
|
|
||||||
|
|||||||
@@ -1944,11 +1944,17 @@ class TestProductionPlan(IntegrationTestCase):
|
|||||||
|
|
||||||
mr_items = get_items_for_material_requests(plan.as_dict())
|
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)
|
# 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)
|
# 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):
|
def test_stock_reservation_against_production_plan(self):
|
||||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
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):
|
def test_production_plan_for_partial_sub_assembly_items(self):
|
||||||
from erpnext.controllers.status_updater import OverAllowanceError
|
from erpnext.controllers.status_updater import OverAllowanceError
|
||||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
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
|
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,
|
"skip_available_sub_assembly_item": args.skip_available_sub_assembly_item or 0,
|
||||||
"sub_assembly_warehouse": args.sub_assembly_warehouse,
|
"sub_assembly_warehouse": args.sub_assembly_warehouse,
|
||||||
"reserve_stock": args.reserve_stock or 0,
|
"reserve_stock": args.reserve_stock or 0,
|
||||||
|
"for_warehouse": args.for_warehouse or None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -79,13 +79,14 @@
|
|||||||
"fieldname": "received_qty",
|
"fieldname": "received_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Received Qty",
|
"label": "Received Qty",
|
||||||
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "bom_no",
|
"fieldname": "bom_no",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Bom No",
|
"label": "BOM No",
|
||||||
"options": "BOM"
|
"options": "BOM"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -245,7 +246,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-06-10 13:36:24.759101",
|
"modified": "2025-11-03 14:33:50.677717",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Production Plan Sub Assembly Item",
|
"name": "Production Plan Sub Assembly Item",
|
||||||
|
|||||||
@@ -2502,18 +2502,11 @@ def get_auto_batch_nos(kwargs):
|
|||||||
|
|
||||||
|
|
||||||
def get_batch_nos_from_sre(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")
|
table = frappe.qb.DocType("Stock Reservation Entry")
|
||||||
child_table = frappe.qb.DocType("Serial and Batch 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 = (
|
query = (
|
||||||
frappe.qb.from_(table)
|
frappe.qb.from_(table)
|
||||||
.join(child_table)
|
.join(child_table)
|
||||||
@@ -2522,7 +2515,6 @@ def get_batch_nos_from_sre(kwargs):
|
|||||||
child_table.batch_no,
|
child_table.batch_no,
|
||||||
child_table.warehouse,
|
child_table.warehouse,
|
||||||
Sum(child_table.qty - child_table.delivered_qty).as_("qty"),
|
Sum(child_table.qty - child_table.delivered_qty).as_("qty"),
|
||||||
creation_field,
|
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(table.docstatus == 1)
|
(table.docstatus == 1)
|
||||||
@@ -2530,7 +2522,6 @@ def get_batch_nos_from_sre(kwargs):
|
|||||||
& (child_table.qty != child_table.delivered_qty)
|
& (child_table.qty != child_table.delivered_qty)
|
||||||
)
|
)
|
||||||
.groupby(child_table.batch_no, child_table.warehouse)
|
.groupby(child_table.batch_no, child_table.warehouse)
|
||||||
.orderby("sort_creation", order=order)
|
|
||||||
.orderby(child_table.batch_no, order=frappe.query_builder.Order.asc)
|
.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"):
|
for item in self.get("items"):
|
||||||
item.update(get_bin_details(item.item_code, item.s_warehouse))
|
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):
|
def before_validate(self):
|
||||||
from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule
|
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_work_order()
|
||||||
self.update_disassembled_order()
|
self.update_disassembled_order()
|
||||||
self.adjust_stock_reservation_entries_for_return()
|
self.adjust_stock_reservation_entries_for_return()
|
||||||
self.update_sre_for_subcontracting_delivery()
|
self.update_stock_reservation_entries()
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
self.make_stock_reserve_for_wip_and_fg()
|
self.make_stock_reserve_for_wip_and_fg()
|
||||||
|
self.reserve_stock_for_subcontracting()
|
||||||
|
|
||||||
self.update_subcontract_order_supplied_items()
|
self.update_subcontract_order_supplied_items()
|
||||||
self.update_subcontracting_order_status()
|
self.update_subcontracting_order_status()
|
||||||
@@ -324,7 +331,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
|||||||
self.update_transferred_qty()
|
self.update_transferred_qty()
|
||||||
self.update_quality_inspection()
|
self.update_quality_inspection()
|
||||||
self.adjust_stock_reservation_entries_for_return()
|
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_auto_created_batches()
|
||||||
self.delete_linked_stock_entry()
|
self.delete_linked_stock_entry()
|
||||||
|
|
||||||
@@ -1889,6 +1896,30 @@ class StockEntry(StockController, SubcontractingInwardController):
|
|||||||
|
|
||||||
pro_doc.set_reserved_qty_for_wip_and_fg(self)
|
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):
|
def cancel_stock_reserve_for_wip_and_fg(self):
|
||||||
if self.is_stock_reserve_for_work_order():
|
if self.is_stock_reserve_for_work_order():
|
||||||
pro_doc = frappe.get_doc("Work Order", self.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)
|
self.calculate_rate_and_amount(raise_error_if_no_rate=False)
|
||||||
|
|
||||||
def set_serial_batch_from_reserved_entry(self):
|
def set_serial_batch_from_reserved_entry(self):
|
||||||
if not self.work_order:
|
if self.work_order and 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 not frappe.get_cached_value("Work Order", self.work_order, "reserve_stock"):
|
if (
|
||||||
return
|
self.purpose not in ["Material Transfer for Manufacture"]
|
||||||
|
and frappe.db.get_single_value("Manufacturing Settings", "backflush_raw_materials_based_on")
|
||||||
skip_transfer = frappe.get_cached_value("Work Order", self.work_order, "skip_transfer")
|
!= "BOM"
|
||||||
|
and not skip_transfer
|
||||||
if (
|
):
|
||||||
self.purpose not in ["Material Transfer for Manufacture"]
|
return
|
||||||
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()
|
reservation_entries = self.get_available_reserved_materials()
|
||||||
if not reservation_entries:
|
if not reservation_entries:
|
||||||
@@ -2252,6 +2278,9 @@ class StockEntry(StockController, SubcontractingInwardController):
|
|||||||
|
|
||||||
new_items_to_add = []
|
new_items_to_add = []
|
||||||
for d in self.items:
|
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)
|
key = (d.item_code, d.s_warehouse)
|
||||||
if details := reservation_entries.get(key):
|
if details := reservation_entries.get(key):
|
||||||
original_qty = d.qty
|
original_qty = d.qty
|
||||||
@@ -2363,7 +2392,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
|||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(doctype.docstatus == 1)
|
(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)
|
& (serial_batch_doc.delivered_qty < serial_batch_doc.qty)
|
||||||
)
|
)
|
||||||
.orderby(serial_batch_doc.idx)
|
.orderby(serial_batch_doc.idx)
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"oldfieldname": "voucher_type",
|
"oldfieldname": "voucher_type",
|
||||||
"oldfieldtype": "Data",
|
"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",
|
"print_width": "150px",
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"width": "150px"
|
"width": "150px"
|
||||||
@@ -315,8 +315,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "production_section",
|
"fieldname": "production_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break"
|
||||||
"label": "Production"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_qdwj",
|
"fieldname": "column_break_qdwj",
|
||||||
@@ -335,7 +334,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "transferred_qty",
|
"fieldname": "transferred_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Qty in WIP Warehouse"
|
"label": "Transferred Qty"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -344,7 +343,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-12 19:48:33.170835",
|
"modified": "2025-11-10 16:09:10.380024",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Reservation Entry",
|
"name": "Stock Reservation Entry",
|
||||||
|
|||||||
@@ -64,7 +64,12 @@ class StockReservationEntry(Document):
|
|||||||
voucher_no: DF.DynamicLink | None
|
voucher_no: DF.DynamicLink | None
|
||||||
voucher_qty: DF.Float
|
voucher_qty: DF.Float
|
||||||
voucher_type: DF.Literal[
|
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
|
warehouse: DF.Link | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
@@ -338,7 +343,7 @@ class StockReservationEntry(Document):
|
|||||||
|
|
||||||
def validate_reservation_based_on_serial_and_batch(self) -> None:
|
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`."""
|
"""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
|
return
|
||||||
|
|
||||||
if self.reservation_based_on == "Serial and Batch":
|
if self.reservation_based_on == "Serial and Batch":
|
||||||
@@ -460,13 +465,14 @@ class StockReservationEntry(Document):
|
|||||||
"Sales Order": "Sales Order Item",
|
"Sales Order": "Sales Order Item",
|
||||||
"Work Order": "Work Order Item",
|
"Work Order": "Work Order Item",
|
||||||
"Production Plan": "Production Plan Sub Assembly Item",
|
"Production Plan": "Production Plan Sub Assembly Item",
|
||||||
|
"Subcontracting Order": "Subcontracting Order Supplied Item",
|
||||||
}.get(self.voucher_type, None)
|
}.get(self.voucher_type, None)
|
||||||
|
|
||||||
if item_doctype:
|
if item_doctype:
|
||||||
sre = frappe.qb.DocType("Stock Reservation Entry")
|
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||||
reserved_qty = (
|
reserved_qty = (
|
||||||
frappe.qb.from_(sre)
|
frappe.qb.from_(sre)
|
||||||
.select(Sum(sre.reserved_qty))
|
.select(Sum(sre.reserved_qty - sre.delivered_qty - sre.transferred_qty - sre.consumed_qty))
|
||||||
.where(
|
.where(
|
||||||
(sre.docstatus == 1)
|
(sre.docstatus == 1)
|
||||||
& (sre.voucher_type == self.voucher_type)
|
& (sre.voucher_type == self.voucher_type)
|
||||||
@@ -574,7 +580,7 @@ class StockReservationEntry(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
from_voucher_detail_no = None
|
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
|
from_voucher_detail_no = self.from_voucher_detail_no
|
||||||
|
|
||||||
total_reserved_qty = get_sre_reserved_qty_for_voucher_detail_no(
|
total_reserved_qty = get_sre_reserved_qty_for_voucher_detail_no(
|
||||||
@@ -1276,7 +1282,7 @@ class StockReservation:
|
|||||||
if not reservation_entries:
|
if not reservation_entries:
|
||||||
return
|
return
|
||||||
|
|
||||||
entries_to_reserve = frappe._dict({})
|
entries_to_reserve = frappe._dict()
|
||||||
for row in reservation_entries:
|
for row in reservation_entries:
|
||||||
reserved_qty_field = "reserved_qty" if row.reservation_based_on == "Qty" else "sabb_qty"
|
reserved_qty_field = "reserved_qty" if row.reservation_based_on == "Qty" else "sabb_qty"
|
||||||
delivered_qty_field = (
|
delivered_qty_field = (
|
||||||
@@ -1293,7 +1299,7 @@ class StockReservation:
|
|||||||
if available_qty <= 0:
|
if available_qty <= 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
key = (row.item_code, row.warehouse)
|
key = (row.item_code, row.warehouse, entry.voucher_detail_no)
|
||||||
|
|
||||||
if key not in entries_to_reserve:
|
if key not in entries_to_reserve:
|
||||||
entries_to_reserve.setdefault(
|
entries_to_reserve.setdefault(
|
||||||
@@ -1303,7 +1309,7 @@ class StockReservation:
|
|||||||
"qty_to_reserve": 0.0,
|
"qty_to_reserve": 0.0,
|
||||||
"item_code": row.item_code,
|
"item_code": row.item_code,
|
||||||
"warehouse": row.warehouse,
|
"warehouse": row.warehouse,
|
||||||
"voucher_type": entry.voucher_type,
|
"voucher_type": entry.voucher_type or to_doctype,
|
||||||
"voucher_no": entry.voucher_no,
|
"voucher_no": entry.voucher_no,
|
||||||
"voucher_detail_no": entry.voucher_detail_no,
|
"voucher_detail_no": entry.voucher_detail_no,
|
||||||
"serial_nos": [],
|
"serial_nos": [],
|
||||||
@@ -1475,6 +1481,9 @@ class StockReservation:
|
|||||||
.orderby(sabb_entry.idx)
|
.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:
|
if against_fg_item:
|
||||||
query = query.where(
|
query = query.where(
|
||||||
sre.voucher_detail_no.isin(
|
sre.voucher_detail_no.isin(
|
||||||
@@ -1490,9 +1499,14 @@ class StockReservation:
|
|||||||
|
|
||||||
def get_items_to_reserve(self, docnames, from_doctype, to_doctype):
|
def get_items_to_reserve(self, docnames, from_doctype, to_doctype):
|
||||||
field = frappe.scrub(from_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)
|
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 = (
|
query = (
|
||||||
frappe.qb.from_(doctype)
|
frappe.qb.from_(doctype)
|
||||||
@@ -1501,11 +1515,12 @@ class StockReservation:
|
|||||||
.select(
|
.select(
|
||||||
doctype.name.as_("voucher_no"),
|
doctype.name.as_("voucher_no"),
|
||||||
child_doctype.name.as_("voucher_detail_no"),
|
child_doctype.name.as_("voucher_detail_no"),
|
||||||
child_doctype.item_code,
|
child_doctype[item_code_fieldname].as_("item_code"),
|
||||||
doctype.company,
|
doctype.company,
|
||||||
child_doctype.stock_uom,
|
child_doctype.stock_uom,
|
||||||
)
|
)
|
||||||
.where((doctype.docstatus == 1) & (doctype[field].isin(docnames)))
|
.where((doctype.docstatus == 1) & (doctype[field].isin(docnames)))
|
||||||
|
.groupby(child_doctype.name)
|
||||||
)
|
)
|
||||||
|
|
||||||
if to_doctype == "Work Order":
|
if to_doctype == "Work Order":
|
||||||
@@ -1523,6 +1538,15 @@ class StockReservation:
|
|||||||
(doctype.qty > doctype.material_transferred_for_manufacturing)
|
(doctype.qty > doctype.material_transferred_for_manufacturing)
|
||||||
& (doctype.status != "Completed")
|
& (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)
|
data = query.run(as_dict=True)
|
||||||
items = []
|
items = []
|
||||||
|
|||||||
@@ -172,11 +172,279 @@ frappe.ui.form.on("Subcontracting Order", {
|
|||||||
__("Status")
|
__("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");
|
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) {
|
update_subcontracting_order_status(frm, status) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.update_subcontracting_order_status",
|
method: "erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.update_subcontracting_order_status",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"service_items",
|
"service_items",
|
||||||
"raw_materials_supplied_section",
|
"raw_materials_supplied_section",
|
||||||
"set_reserve_warehouse",
|
"set_reserve_warehouse",
|
||||||
|
"reserve_stock",
|
||||||
"supplied_items",
|
"supplied_items",
|
||||||
"tab_address_and_contact",
|
"tab_address_and_contact",
|
||||||
"supplier_address",
|
"supplier_address",
|
||||||
@@ -62,7 +63,8 @@
|
|||||||
"select_print_heading",
|
"select_print_heading",
|
||||||
"column_break_43",
|
"column_break_43",
|
||||||
"letter_head",
|
"letter_head",
|
||||||
"tab_connections"
|
"tab_connections",
|
||||||
|
"production_plan"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -471,6 +473,22 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Currency",
|
"options": "Currency",
|
||||||
"read_only": 1
|
"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",
|
"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.buying.utils import check_on_hold_or_closed_status
|
||||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
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.stock_balance import update_bin_qty
|
||||||
from erpnext.stock.utils import get_bin
|
from erpnext.stock.utils import get_bin
|
||||||
|
|
||||||
@@ -50,8 +54,10 @@ class SubcontractingOrder(SubcontractingController):
|
|||||||
letter_head: DF.Link | None
|
letter_head: DF.Link | None
|
||||||
naming_series: DF.Literal["SC-ORD-.YYYY.-"]
|
naming_series: DF.Literal["SC-ORD-.YYYY.-"]
|
||||||
per_received: DF.Percent
|
per_received: DF.Percent
|
||||||
|
production_plan: DF.Data | None
|
||||||
project: DF.Link | None
|
project: DF.Link | None
|
||||||
purchase_order: DF.Link
|
purchase_order: DF.Link
|
||||||
|
reserve_stock: DF.Check
|
||||||
schedule_date: DF.Date | None
|
schedule_date: DF.Date | None
|
||||||
select_print_heading: DF.Link | None
|
select_print_heading: DF.Link | None
|
||||||
service_items: DF.Table[SubcontractingOrderServiceItem]
|
service_items: DF.Table[SubcontractingOrderServiceItem]
|
||||||
@@ -105,6 +111,13 @@ class SubcontractingOrder(SubcontractingController):
|
|||||||
frappe.db.get_single_value("Buying Settings", "over_transfer_allowance"),
|
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):
|
def before_validate(self):
|
||||||
super().before_validate()
|
super().before_validate()
|
||||||
|
|
||||||
@@ -121,6 +134,7 @@ class SubcontractingOrder(SubcontractingController):
|
|||||||
self.update_prevdoc_status()
|
self.update_prevdoc_status()
|
||||||
self.update_status()
|
self.update_status()
|
||||||
self.update_subcontracted_quantity_in_po()
|
self.update_subcontracted_quantity_in_po()
|
||||||
|
self.reserve_raw_materials()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.update_prevdoc_status()
|
self.update_prevdoc_status()
|
||||||
@@ -253,10 +267,10 @@ class SubcontractingOrder(SubcontractingController):
|
|||||||
if si.fg_item:
|
if si.fg_item:
|
||||||
item = frappe.get_doc("Item", 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",
|
"Purchase Order Item",
|
||||||
si.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)
|
available_qty = flt(qty) - flt(subcontracted_qty)
|
||||||
|
|
||||||
@@ -292,6 +306,7 @@ class SubcontractingOrder(SubcontractingController):
|
|||||||
"purchase_order_item": si.purchase_order_item,
|
"purchase_order_item": si.purchase_order_item,
|
||||||
"material_request": si.material_request,
|
"material_request": si.material_request,
|
||||||
"material_request_item": si.material_request_item,
|
"material_request_item": si.material_request_item,
|
||||||
|
"production_plan_sub_assembly_item": production_plan_sub_assembly_item,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -362,6 +377,90 @@ class SubcontractingOrder(SubcontractingController):
|
|||||||
subcontracted_qty,
|
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()
|
@frappe.whitelist()
|
||||||
def make_subcontracting_receipt(source_name, target_doc=None):
|
def make_subcontracting_receipt(source_name, target_doc=None):
|
||||||
|
|||||||
@@ -4,5 +4,15 @@ from frappe import _
|
|||||||
def get_data():
|
def get_data():
|
||||||
return {
|
return {
|
||||||
"fieldname": "subcontracting_order",
|
"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)
|
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):
|
def create_subcontracting_order(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -55,7 +55,8 @@
|
|||||||
"section_break_34",
|
"section_break_34",
|
||||||
"purchase_order_item",
|
"purchase_order_item",
|
||||||
"page_break",
|
"page_break",
|
||||||
"subcontracting_conversion_factor"
|
"subcontracting_conversion_factor",
|
||||||
|
"production_plan_sub_assembly_item"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -407,6 +408,16 @@
|
|||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Subcontracting Conversion Factor",
|
"label": "Subcontracting Conversion Factor",
|
||||||
"read_only": 1
|
"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,
|
"grid_page_length": 50,
|
||||||
@@ -414,7 +425,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-08-10 22:37:39.863628",
|
"modified": "2025-11-03 12:29:45.156101",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Subcontracting",
|
"module": "Subcontracting",
|
||||||
"name": "Subcontracting Order Item",
|
"name": "Subcontracting Order Item",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class SubcontractingOrderItem(Document):
|
|||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
production_plan_sub_assembly_item: DF.Data | None
|
||||||
project: DF.Link | None
|
project: DF.Link | None
|
||||||
purchase_order_item: DF.Data | None
|
purchase_order_item: DF.Data | None
|
||||||
qty: DF.Float
|
qty: DF.Float
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"section_break_13",
|
"section_break_13",
|
||||||
"required_qty",
|
"required_qty",
|
||||||
"supplied_qty",
|
"supplied_qty",
|
||||||
|
"stock_reserved_qty",
|
||||||
"column_break_16",
|
"column_break_16",
|
||||||
"consumed_qty",
|
"consumed_qty",
|
||||||
"returned_qty",
|
"returned_qty",
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "stock_uom",
|
"fieldname": "stock_uom",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Stock Uom",
|
"label": "Stock UOM",
|
||||||
"options": "UOM",
|
"options": "UOM",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -160,18 +161,29 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 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,
|
"hide_toolbar": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:46.680164",
|
"modified": "2025-10-30 16:00:43.379828",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Subcontracting",
|
"module": "Subcontracting",
|
||||||
"name": "Subcontracting Order Supplied Item",
|
"name": "Subcontracting Order Supplied Item",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class SubcontractingOrderSuppliedItem(Document):
|
|||||||
reserve_warehouse: DF.Link | None
|
reserve_warehouse: DF.Link | None
|
||||||
returned_qty: DF.Float
|
returned_qty: DF.Float
|
||||||
rm_item_code: DF.Link | None
|
rm_item_code: DF.Link | None
|
||||||
|
stock_reserved_qty: DF.Float
|
||||||
stock_uom: DF.Link | None
|
stock_uom: DF.Link | None
|
||||||
supplied_qty: DF.Float
|
supplied_qty: DF.Float
|
||||||
total_supplied_qty: DF.Float
|
total_supplied_qty: DF.Float
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
|
|
||||||
for table_name in ["items", "supplied_items"]:
|
for table_name in ["items", "supplied_items"]:
|
||||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||||
|
|
||||||
|
self.update_stock_reservation_entries()
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
self.repost_future_sle_and_gle()
|
self.repost_future_sle_and_gle()
|
||||||
@@ -189,6 +191,7 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
self.set_consumed_qty_in_subcontract_order()
|
self.set_consumed_qty_in_subcontract_order()
|
||||||
self.set_subcontracting_order_status(update_bin=False)
|
self.set_subcontracting_order_status(update_bin=False)
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
|
self.update_stock_reservation_entries()
|
||||||
self.make_gl_entries_on_cancel()
|
self.make_gl_entries_on_cancel()
|
||||||
self.repost_future_sle_and_gle()
|
self.repost_future_sle_and_gle()
|
||||||
self.update_status()
|
self.update_status()
|
||||||
@@ -199,7 +202,7 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
def reset_raw_materials(self):
|
def reset_raw_materials(self):
|
||||||
self.supplied_items = []
|
self.supplied_items = []
|
||||||
self.flags.reset_raw_materials = True
|
self.flags.reset_raw_materials = True
|
||||||
self.create_raw_materials_supplied()
|
self.create_raw_materials_supplied_or_received()
|
||||||
|
|
||||||
def validate_closed_subcontracting_order(self):
|
def validate_closed_subcontracting_order(self):
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
@@ -853,6 +856,17 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
if frappe.db.get_single_value("Buying Settings", "auto_create_purchase_receipt"):
|
if frappe.db.get_single_value("Buying Settings", "auto_create_purchase_receipt"):
|
||||||
make_purchase_receipt(self, save=True, notify=True)
|
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()
|
@frappe.whitelist()
|
||||||
def make_subcontract_return_against_rejected_warehouse(source_name):
|
def make_subcontract_return_against_rejected_warehouse(source_name):
|
||||||
|
|||||||
Reference in New Issue
Block a user