fix: inward same serial / batches in disassembly which were used

(cherry picked from commit 95e6c72539)

# Conflicts:
#	erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
#	erpnext/stock/doctype/stock_entry/stock_entry.py
This commit is contained in:
Rohit Waghchaure
2025-11-25 15:21:35 +05:30
committed by Mergify
parent 3a2d7d18a3
commit cfbd71693b
3 changed files with 126 additions and 37 deletions

View File

@@ -229,7 +229,8 @@ frappe.ui.form.on("Work Order", {
if (
frm.doc.docstatus === 1 &&
["Closed", "Completed"].includes(frm.doc.status) &&
frm.doc.produced_qty > 0
frm.doc.produced_qty > 0 &&
frm.doc.produced_qty > frm.doc.disassembled_qty
) {
frm.add_custom_button(
__("Disassemble Order"),
@@ -406,7 +407,6 @@ frappe.ui.form.on("Work Order", {
work_order_id: frm.doc.name,
purpose: "Disassemble",
qty: data.qty,
target_warehouse: data.target_warehouse,
});
})
.then((stock_entry) => {
@@ -863,24 +863,6 @@ erpnext.work_order = {
},
];
if (purpose === "Disassemble") {
fields.push({
fieldtype: "Link",
options: "Warehouse",
fieldname: "target_warehouse",
label: __("Target Warehouse"),
default: frm.doc.source_warehouse || frm.doc.wip_warehouse,
get_query() {
return {
filters: {
company: frm.doc.company,
is_group: 0,
},
};
},
});
}
return new Promise((resolve, reject) => {
frm.qty_prompt = frappe.prompt(
fields,

View File

@@ -2532,6 +2532,9 @@ def get_voucher_wise_serial_batch_from_bundle(**kwargs) -> dict[str, dict]:
child_row = group_by_voucher[key]
if row.serial_no:
child_row["serial_nos"].append(row.serial_no)
child_row["item_row"].qty = len(child_row["serial_nos"]) * (
-1 if row.type_of_transaction == "Outward" else 1
)
if row.batch_no:
child_row["batch_nos"][row.batch_no] += row.qty
@@ -2652,8 +2655,13 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]:
serial_batch_table.incoming_rate,
bundle_table.voucher_detail_no,
bundle_table.voucher_no,
<<<<<<< HEAD
bundle_table.posting_date,
bundle_table.posting_time,
=======
bundle_table.posting_datetime,
bundle_table.type_of_transaction,
>>>>>>> 95e6c72539 (fix: inward same serial / batches in disassembly which were used)
)
.where(
(bundle_table.docstatus == 1)

View File

@@ -243,8 +243,75 @@ class StockEntry(StockController):
self.validate_same_source_target_warehouse_during_material_transfer()
<<<<<<< HEAD
def on_submit(self):
self.validate_closed_subcontracting_order()
=======
self.validate_closed_subcontracting_order()
self.validate_subcontract_order()
super().validate_subcontracting_inward()
def set_serial_batch_for_disassembly(self):
if self.purpose != "Disassemble":
return
available_materials = get_available_materials(self.work_order, self)
for row in self.items:
warehouse = row.s_warehouse or row.t_warehouse
materials = available_materials.get((row.item_code, warehouse))
if not materials:
continue
batches = defaultdict(float)
serial_nos = []
qty = row.transfer_qty
for batch_no, batch_qty in materials.batch_details.items():
if qty <= 0:
break
batch_qty = abs(batch_qty)
if batch_qty <= qty:
batches[batch_no] = batch_qty
qty -= batch_qty
else:
batches[batch_no] = qty
qty = 0
if materials.serial_nos:
serial_nos = materials.serial_nos[: int(row.transfer_qty)]
if not serial_nos and not batches:
continue
bundle_doc = SerialBatchCreation(
{
"item_code": row.item_code,
"warehouse": warehouse,
"posting_datetime": get_combine_datetime(self.posting_date, self.posting_time),
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": row.name,
"qty": row.transfer_qty,
"type_of_transaction": "Inward" if row.t_warehouse else "Outward",
"company": self.company,
"do_not_submit": True,
}
).make_serial_and_batch_bundle(serial_nos=serial_nos, batch_nos=batches)
row.serial_and_batch_bundle = bundle_doc.name
row.use_serial_batch_fields = 0
row.db_set(
{
"serial_and_batch_bundle": bundle_doc.name,
"use_serial_batch_fields": 0,
}
)
def on_submit(self):
self.set_serial_batch_for_disassembly()
>>>>>>> 95e6c72539 (fix: inward same serial / batches in disassembly which were used)
self.make_bundle_using_old_serial_batch_fields()
self.update_disassembled_order()
self.update_stock_ledger()
@@ -1856,7 +1923,13 @@ class StockEntry(StockController):
s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse")
items_dict = get_bom_items_as_dict(self.bom_no, self.company, disassemble_qty)
items_dict = get_bom_items_as_dict(
self.bom_no,
self.company,
disassemble_qty,
fetch_exploded=self.use_multi_level_bom,
fetch_qty_in_stock_uom=False,
)
for row in items:
child_row = self.append("items", {})
@@ -1874,7 +1947,7 @@ class StockEntry(StockController):
child_row.qty = disassemble_qty
child_row.s_warehouse = (self.from_warehouse or s_warehouse) if row.is_finished_item else ""
child_row.t_warehouse = self.to_warehouse if not row.is_finished_item else ""
child_row.t_warehouse = row.s_warehouse
child_row.is_finished_item = 0 if row.is_finished_item else 1
def get_items_from_manufacture_entry(self):
@@ -1893,6 +1966,8 @@ class StockEntry(StockController):
"`tabStock Entry Detail`.`is_finished_item`",
"`tabStock Entry Detail`.`batch_no`",
"`tabStock Entry Detail`.`serial_no`",
"`tabStock Entry Detail`.`s_warehouse`",
"`tabStock Entry Detail`.`t_warehouse`",
"`tabStock Entry Detail`.`use_serial_batch_fields`",
],
filters=[
@@ -3259,8 +3334,8 @@ def get_items_from_subcontract_order(source_name, target_doc=None):
return target_doc
def get_available_materials(work_order) -> dict:
data = get_stock_entry_data(work_order)
def get_available_materials(work_order, stock_entry_doc=None) -> dict:
data = get_stock_entry_data(work_order, stock_entry_doc=stock_entry_doc)
available_materials = {}
for row in data:
@@ -3268,6 +3343,9 @@ def get_available_materials(work_order) -> dict:
if row.purpose != "Material Transfer for Manufacture":
key = (row.item_code, row.s_warehouse)
if stock_entry_doc and stock_entry_doc.purpose == "Disassemble":
key = (row.item_code, row.s_warehouse or row.warehouse)
if key not in available_materials:
available_materials.setdefault(
key,
@@ -3278,7 +3356,9 @@ def get_available_materials(work_order) -> dict:
item_data = available_materials[key]
if row.purpose == "Material Transfer for Manufacture":
if row.purpose == "Material Transfer for Manufacture" or (
stock_entry_doc and stock_entry_doc.purpose == "Disassemble" and row.purpose == "Manufacture"
):
item_data.qty += row.qty
if row.batch_no:
item_data.batch_details[row.batch_no] += row.qty
@@ -3318,7 +3398,7 @@ def get_available_materials(work_order) -> dict:
return available_materials
def get_stock_entry_data(work_order):
def get_stock_entry_data(work_order, stock_entry_doc=None):
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_voucher_wise_serial_batch_from_bundle,
)
@@ -3350,19 +3430,35 @@ def get_stock_entry_data(work_order):
(stock_entry.name == stock_entry_detail.parent)
& (stock_entry.work_order == work_order)
& (stock_entry.docstatus == 1)
& (stock_entry_detail.s_warehouse.isnotnull())
& (
stock_entry.purpose.isin(
[
"Manufacture",
"Material Consumption for Manufacture",
"Material Transfer for Manufacture",
]
)
)
)
.orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx)
).run(as_dict=1)
)
if stock_entry_doc and stock_entry_doc.purpose == "Disassemble":
data = data.where(
stock_entry.purpose.isin(
[
"Disassemble",
"Manufacture",
]
)
)
data = data.where(stock_entry.name != stock_entry_doc.name)
else:
data = data.where(
stock_entry.purpose.isin(
[
"Manufacture",
"Material Consumption for Manufacture",
"Material Transfer for Manufacture",
]
)
)
data = data.where(stock_entry_detail.s_warehouse.isnotnull())
data = data.run(as_dict=1)
if not data:
return []
@@ -3375,6 +3471,9 @@ def get_stock_entry_data(work_order):
if row.purpose != "Material Transfer for Manufacture":
key = (row.item_code, row.s_warehouse, row.name)
if stock_entry_doc and stock_entry_doc.purpose == "Disassemble":
key = (row.item_code, row.s_warehouse or row.warehouse, row.name)
if bundle_data.get(key):
row.update(bundle_data.get(key))