diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f1df54dd6a..293c2e54f5 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1656,118 +1656,58 @@ class StockEntry(StockController): ) def get_transfered_raw_materials(self): - transferred_materials = frappe.db.sql( - """ - select - item_name, original_item, item_code, sum(qty) as qty, sed.t_warehouse as warehouse, - description, stock_uom, expense_account, cost_center - from `tabStock Entry` se,`tabStock Entry Detail` sed - where - se.name = sed.parent and se.docstatus=1 and se.purpose='Material Transfer for Manufacture' - and se.work_order= %s and ifnull(sed.t_warehouse, '') != '' - group by sed.item_code, sed.t_warehouse - """, + available_materials = get_available_materials(self.work_order) + + wo_data = frappe.db.get_value( + "Work Order", self.work_order, + ["qty", "produced_qty", "material_transferred_for_manufacturing as trans_qty"], as_dict=1, ) - materials_already_backflushed = frappe.db.sql( - """ - select - item_code, sed.s_warehouse as warehouse, sum(qty) as qty - from - `tabStock Entry` se, `tabStock Entry Detail` sed - where - se.name = sed.parent and se.docstatus=1 - and (se.purpose='Manufacture' or se.purpose='Material Consumption for Manufacture') - and se.work_order= %s and ifnull(sed.s_warehouse, '') != '' - group by sed.item_code, sed.s_warehouse - """, - self.work_order, - as_dict=1, - ) - - backflushed_materials = {} - for d in materials_already_backflushed: - backflushed_materials.setdefault(d.item_code, []).append({d.warehouse: d.qty}) - - po_qty = frappe.db.sql( - """select qty, produced_qty, material_transferred_for_manufacturing from - `tabWork Order` where name=%s""", - self.work_order, - as_dict=1, - )[0] - - manufacturing_qty = flt(po_qty.qty) or 1 - produced_qty = flt(po_qty.produced_qty) - trans_qty = flt(po_qty.material_transferred_for_manufacturing) or 1 - - for item in transferred_materials: - qty = item.qty - item_code = item.original_item or item.item_code - req_items = frappe.get_all( - "Work Order Item", - filters={"parent": self.work_order, "item_code": item_code}, - fields=["required_qty", "consumed_qty"], + for key, row in available_materials.items(): + qty = (flt(row.qty) * flt(self.fg_completed_qty)) / ( + flt(wo_data.trans_qty) - flt(wo_data.produced_qty) ) - req_qty = flt(req_items[0].required_qty) if req_items else flt(4) - req_qty_each = flt(req_qty / manufacturing_qty) - consumed_qty = flt(req_items[0].consumed_qty) if req_items else 0 - - if trans_qty and manufacturing_qty > (produced_qty + flt(self.fg_completed_qty)): - if qty >= req_qty: - qty = (req_qty / trans_qty) * flt(self.fg_completed_qty) - else: - qty = qty - consumed_qty - - if self.purpose == "Manufacture": - # If Material Consumption is booked, must pull only remaining components to finish product - if consumed_qty != 0: - remaining_qty = consumed_qty - (produced_qty * req_qty_each) - exhaust_qty = req_qty_each * produced_qty - if remaining_qty > exhaust_qty: - if (remaining_qty / (req_qty_each * flt(self.fg_completed_qty))) >= 1: - qty = 0 - else: - qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty - else: - if self.flags.backflush_based_on == "Material Transferred for Manufacture": - qty = (item.qty / trans_qty) * flt(self.fg_completed_qty) - else: - qty = req_qty_each * flt(self.fg_completed_qty) - - elif backflushed_materials.get(item.item_code): - precision = frappe.get_precision("Stock Entry Detail", "qty") - for d in backflushed_materials.get(item.item_code): - if d.get(item.warehouse) > 0: - if qty > req_qty: - qty = ( - (flt(qty, precision) - flt(d.get(item.warehouse), precision)) - / (flt(trans_qty, precision) - flt(produced_qty, precision)) - ) * flt(self.fg_completed_qty) - - d[item.warehouse] -= qty - + item = row.item_details if cint(frappe.get_cached_value("UOM", item.stock_uom, "must_be_whole_number")): qty = frappe.utils.ceil(qty) - if qty > 0: - self.add_to_stock_entry_detail( - { - item.item_code: { - "from_warehouse": item.warehouse, - "to_warehouse": "", - "qty": qty, - "item_name": item.item_name, - "description": item.description, - "stock_uom": item.stock_uom, - "expense_account": item.expense_account, - "cost_center": item.buying_cost_center, - "original_item": item.original_item, - } - } - ) + if row.batch_details: + for batch_no, batch_qty in row.batch_details.items(): + if qty <= 0: + continue + + if batch_qty > qty: + batch_qty = qty + + item.batch_no = batch_no + self.update_item_in_stock_entry_detail(row, item, batch_qty) + + row.batch_details[batch_no] -= batch_qty + qty -= batch_qty + else: + self.update_item_in_stock_entry_detail(row, item, qty) + + def update_item_in_stock_entry_detail(self, row, item, qty): + ste_item_details = { + "from_warehouse": item.warehouse, + "to_warehouse": "", + "qty": qty, + "item_name": item.item_name, + "batch_no": item.batch_no, + "description": item.description, + "stock_uom": item.stock_uom, + "expense_account": item.expense_account, + "cost_center": item.buying_cost_center, + "original_item": item.original_item, + } + + if item.serial_no: + ste_item_details["serial_no"] = "\n".join(item.serial_no[0 : cint(qty)]) + + self.add_to_stock_entry_detail({item.item_code: ste_item_details}) def get_pending_raw_materials(self, backflush_based_on=None): """ @@ -2521,3 +2461,80 @@ def get_supplied_items(purchase_order): ) return supplied_item_details + + +def get_available_materials(work_order) -> dict: + data = get_stock_entry_data(work_order) + + available_materials = {} + for row in data: + key = (row.item_code, row.warehouse) + if row.purpose != "Material Transfer for Manufacture": + key = (row.item_code, row.s_warehouse) + + if key not in available_materials: + available_materials.setdefault( + key, + frappe._dict( + {"item_details": row, "batch_details": defaultdict(float), "qty": 0, "serial_nos": []} + ), + ) + + item_data = available_materials[key] + + if row.purpose == "Material Transfer for Manufacture": + item_data.qty += row.qty + if row.batch_no: + item_data.batch_details[row.batch_no] += row.qty + + if row.serial_no: + item_data.serial_nos.extend(get_serial_nos(row.serial_no)) + else: + # Consume raw material qty in case of 'Manufacture' or 'Material Consumption for Manufacture' + + item_data.qty -= row.qty + if row.batch_no: + item_data.batch_details[row.batch_no] -= row.qty + + if row.serial_no: + for serial_no in get_serial_nos(row.serial_no): + item_data.serial_nos.remove(serial_no) + + return available_materials + + +def get_stock_entry_data(work_order): + stock_entry = frappe.qb.DocType("Stock Entry") + stock_entry_detail = frappe.qb.DocType("Stock Entry Detail") + + return ( + frappe.qb.from_(stock_entry) + .from_(stock_entry_detail) + .select( + stock_entry_detail.item_name, + stock_entry_detail.original_item, + stock_entry_detail.item_code, + stock_entry_detail.qty, + (stock_entry_detail.t_warehouse).as_("warehouse"), + (stock_entry_detail.s_warehouse).as_("s_warehouse"), + stock_entry_detail.description, + stock_entry_detail.stock_uom, + stock_entry_detail.expense_account, + stock_entry_detail.cost_center, + stock_entry_detail.batch_no, + stock_entry_detail.serial_no, + stock_entry.purpose, + ) + .where( + (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, debug=1)