diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 9c4ceeaee3b..5fe7c607b6c 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -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, diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 46c3b180954..f0e5830c0a7 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -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) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e221595061d..60c14139068 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -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))