diff --git a/erpnext/manufacturing/doctype/work_order/mapper.py b/erpnext/manufacturing/doctype/work_order/mapper.py new file mode 100644 index 00000000000..e2230a1367f --- /dev/null +++ b/erpnext/manufacturing/doctype/work_order/mapper.py @@ -0,0 +1,134 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import json + +import frappe +from frappe.model.mapper import get_mapped_doc +from frappe.utils import flt + + +@frappe.whitelist() +def make_stock_entry( + work_order_id: str, + purpose: str, + qty: float | None = None, + target_warehouse: str | None = None, + is_additional_transfer_entry: bool = False, + source_stock_entry: str | None = None, +): + work_order = frappe.get_doc("Work Order", work_order_id) + if not frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group"): + wip_warehouse = work_order.wip_warehouse + else: + wip_warehouse = None + + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.purpose = purpose + stock_entry.work_order = work_order_id + stock_entry.company = work_order.company + stock_entry.from_bom = 1 + stock_entry.bom_no = work_order.bom_no + stock_entry.use_multi_level_bom = work_order.use_multi_level_bom + if purpose in ["Material Transfer for Manufacture", "Manufacture"]: + stock_entry.subcontracting_inward_order = work_order.subcontracting_inward_order + # accept 0 qty as well + stock_entry.fg_completed_qty = ( + qty if qty is not None else (flt(work_order.qty) - flt(work_order.produced_qty)) + ) + + if purpose == "Material Transfer for Manufacture": + stock_entry.to_warehouse = wip_warehouse + stock_entry.project = work_order.project + else: + stock_entry.from_warehouse = ( + work_order.source_warehouse + if work_order.skip_transfer and not work_order.from_wip_warehouse + else wip_warehouse + ) + stock_entry.to_warehouse = work_order.fg_warehouse + stock_entry.project = work_order.project + if work_order.bom_no: + stock_entry.inspection_required = frappe.db.get_value( + "BOM", work_order.bom_no, "inspection_required" + ) + + if purpose == "Disassemble": + stock_entry.from_warehouse = work_order.fg_warehouse + stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse + if source_stock_entry: + stock_entry.source_stock_entry = source_stock_entry + + stock_entry.set_stock_entry_type() + stock_entry.is_additional_transfer_entry = is_additional_transfer_entry + stock_entry.get_items() + + return stock_entry.as_dict() + + +@frappe.whitelist() +def create_pick_list(source_name: str, target_doc: str | None = None, for_qty: float | None = None): + for_qty = for_qty or json.loads(target_doc).get("for_qty") + max_finished_goods_qty = frappe.db.get_value("Work Order", source_name, "qty") + + def update_item_quantity(source, target, source_parent): + pending_to_issue = flt(source.required_qty) - flt(source.transferred_qty) + desire_to_transfer = flt(source.required_qty) / max_finished_goods_qty * flt(for_qty) + + qty = 0 + if desire_to_transfer <= pending_to_issue: + qty = desire_to_transfer + elif pending_to_issue > 0: + qty = pending_to_issue + + if qty: + target.qty = qty + target.stock_qty = qty + target.uom = frappe.get_value("Item", source.item_code, "stock_uom") + target.stock_uom = target.uom + target.conversion_factor = 1 + else: + target.delete() + + doc = get_mapped_doc( + "Work Order", + source_name, + { + "Work Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}}, + "Work Order Item": { + "doctype": "Pick List Item", + "postprocess": update_item_quantity, + "condition": lambda doc: abs(doc.transferred_qty) < abs(doc.required_qty), + }, + }, + target_doc, + ) + + doc.purpose = "Material Transfer for Manufacture" + doc.for_qty = for_qty + + doc.set_item_locations() + + return doc + + +@frappe.whitelist() +def make_stock_return_entry(work_order: str): + from erpnext.stock.doctype.stock_entry.stock_entry_handler.manufacturing import ( + ManufactureStockEntry, + ) + + wo_doc = frappe.get_cached_doc("Work Order", work_order) + + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.from_bom = 1 + stock_entry.is_return = 1 + stock_entry.work_order = work_order + stock_entry.purpose = "Material Transfer for Manufacture" + stock_entry.bom_no = wo_doc.bom_no + stock_entry.set_stock_entry_type() + + ste_cls = ManufactureStockEntry(stock_entry) + ste_cls.add_raw_materials_based_on_transfer() + ste_cls.return_available_materials_in_source_wh() + return stock_entry diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index e2c221487c0..f46c1b4d83e 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -8,7 +8,6 @@ import frappe from dateutil.relativedelta import relativedelta from frappe import _ from frappe.model.document import Document -from frappe.model.mapper import get_mapped_doc from frappe.query_builder import Case from frappe.query_builder.functions import IfNull, Sum from frappe.utils import ( @@ -41,6 +40,8 @@ from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty from erpnext.stock.utils import get_bin, get_latest_stock_qty, validate_warehouse_company from erpnext.utilities.transaction_base import validate_uom_is_integer +from .mapper import create_pick_list, make_stock_entry, make_stock_return_entry + class OverProductionError(frappe.ValidationError): pass @@ -2401,64 +2402,6 @@ def set_work_order_ops(name: str): po.save() -@frappe.whitelist() -def make_stock_entry( - work_order_id: str, - purpose: str, - qty: float | None = None, - target_warehouse: str | None = None, - is_additional_transfer_entry: bool = False, - source_stock_entry: str | None = None, -): - work_order = frappe.get_doc("Work Order", work_order_id) - if not frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group"): - wip_warehouse = work_order.wip_warehouse - else: - wip_warehouse = None - - stock_entry = frappe.new_doc("Stock Entry") - stock_entry.purpose = purpose - stock_entry.work_order = work_order_id - stock_entry.company = work_order.company - stock_entry.from_bom = 1 - stock_entry.bom_no = work_order.bom_no - stock_entry.use_multi_level_bom = work_order.use_multi_level_bom - if purpose in ["Material Transfer for Manufacture", "Manufacture"]: - stock_entry.subcontracting_inward_order = work_order.subcontracting_inward_order - # accept 0 qty as well - stock_entry.fg_completed_qty = ( - qty if qty is not None else (flt(work_order.qty) - flt(work_order.produced_qty)) - ) - - if purpose == "Material Transfer for Manufacture": - stock_entry.to_warehouse = wip_warehouse - stock_entry.project = work_order.project - else: - stock_entry.from_warehouse = ( - work_order.source_warehouse - if work_order.skip_transfer and not work_order.from_wip_warehouse - else wip_warehouse - ) - stock_entry.to_warehouse = work_order.fg_warehouse - stock_entry.project = work_order.project - if work_order.bom_no: - stock_entry.inspection_required = frappe.db.get_value( - "BOM", work_order.bom_no, "inspection_required" - ) - - if purpose == "Disassemble": - stock_entry.from_warehouse = work_order.fg_warehouse - stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse - if source_stock_entry: - stock_entry.source_stock_entry = source_stock_entry - - stock_entry.set_stock_entry_type() - stock_entry.is_additional_transfer_entry = is_additional_transfer_entry - stock_entry.get_items() - - return stock_entry.as_dict() - - @frappe.whitelist() def get_disassembly_available_qty(stock_entry_name: str, current_se_name: str | None = None) -> float: se = frappe.db.get_value("Stock Entry", stock_entry_name, ["fg_completed_qty"], as_dict=True) @@ -2718,52 +2661,6 @@ def get_work_order_operation_data(work_order, operation, workstation): return d -@frappe.whitelist() -def create_pick_list(source_name: str, target_doc: str | None = None, for_qty: float | None = None): - for_qty = for_qty or json.loads(target_doc).get("for_qty") - max_finished_goods_qty = frappe.db.get_value("Work Order", source_name, "qty") - - def update_item_quantity(source, target, source_parent): - pending_to_issue = flt(source.required_qty) - flt(source.transferred_qty) - desire_to_transfer = flt(source.required_qty) / max_finished_goods_qty * flt(for_qty) - - qty = 0 - if desire_to_transfer <= pending_to_issue: - qty = desire_to_transfer - elif pending_to_issue > 0: - qty = pending_to_issue - - if qty: - target.qty = qty - target.stock_qty = qty - target.uom = frappe.get_value("Item", source.item_code, "stock_uom") - target.stock_uom = target.uom - target.conversion_factor = 1 - else: - target.delete() - - doc = get_mapped_doc( - "Work Order", - source_name, - { - "Work Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}}, - "Work Order Item": { - "doctype": "Pick List Item", - "postprocess": update_item_quantity, - "condition": lambda doc: abs(doc.transferred_qty) < abs(doc.required_qty), - }, - }, - target_doc, - ) - - doc.purpose = "Material Transfer for Manufacture" - doc.for_qty = for_qty - - doc.set_item_locations() - - return doc - - def get_reserved_qty_for_production( item_code: str, warehouse: str, @@ -2813,28 +2710,6 @@ def get_reserved_qty_for_production( return query.run()[0][0] or 0.0 -@frappe.whitelist() -def make_stock_return_entry(work_order: str): - from erpnext.stock.doctype.stock_entry.stock_entry_handler.manufacturing import ( - ManufactureStockEntry, - ) - - wo_doc = frappe.get_cached_doc("Work Order", work_order) - - stock_entry = frappe.new_doc("Stock Entry") - stock_entry.from_bom = 1 - stock_entry.is_return = 1 - stock_entry.work_order = work_order - stock_entry.purpose = "Material Transfer for Manufacture" - stock_entry.bom_no = wo_doc.bom_no - stock_entry.set_stock_entry_type() - - ste_cls = ManufactureStockEntry(stock_entry) - ste_cls.add_raw_materials_based_on_transfer() - ste_cls.return_available_materials_in_source_wh() - return stock_entry - - def get_row_wise_serial_batch(work_order, purpose=None): if not purpose: purpose = "Material Transfer for Manufacture"