diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 7ba01e07851..44cddac46d6 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -580,9 +580,12 @@ frappe.ui.form.on("BOM", { frappe.ui.form.on("BOM Operation", { finished_good(frm, cdt, cdn) { let row = locals[cdt][cdn]; - if (row.finished_good === frm.doc.item) { - frappe.model.set_value(row.doctype, row.name, "is_final_finished_good", 1); - } + frappe.model.set_value( + row.doctype, + row.name, + "is_final_finished_good", + row.finished_good === frm.doc.item + ); }, bom_no(frm, cdt, cdn) { diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index a58a37b92a0..943091487d2 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -296,6 +296,55 @@ class BOM(WebsiteGenerator): self.set_process_loss_qty() self.validate_scrap_items() self.set_default_uom() + self.validate_semi_finished_goods() + self.validate_raw_materials_of_operation() + + def validate_semi_finished_goods(self): + if not self.track_semi_finished_goods or not self.operations: + return + + fg_items = [] + for row in self.operations: + if not row.is_final_finished_good: + continue + + fg_items.append(row.finished_good) + + if not fg_items: + frappe.throw( + _( + "Since you have enabled 'Track Semi Finished Goods', at least one operation must have 'Is Final Finished Good' checked. For that set the FG / Semi FG Item as {0} against an operation." + ).format(bold(self.item)), + ) + + if fg_items and len(fg_items) > 1: + frappe.throw( + _( + "Only one operation can have 'Is Final Finished Good' checked when 'Track Semi Finished Goods' is enabled." + ), + ) + + def validate_raw_materials_of_operation(self): + if not self.track_semi_finished_goods or not self.operations: + return + + operation_idx_with_no_rm = {} + for row in self.operations: + if row.bom_no: + continue + + operation_idx_with_no_rm[row.idx] = row.operation + + for row in self.items: + if row.operation_row_id and row.operation_row_id in operation_idx_with_no_rm: + del operation_idx_with_no_rm[row.operation_row_id] + + for idx, row in operation_idx_with_no_rm.items(): + frappe.throw( + _("For operation {0} at row {1}, please add raw materials or set a BOM against it.").format( + bold(row.operation), idx + ), + ) def set_default_uom(self): if not self.get("items"): diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index ba2a36e2ad3..aa872c13946 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -166,6 +166,25 @@ class JobCard(Document): self.validate_work_order() self.set_employees() + self.validate_semi_finished_goods() + + def validate_semi_finished_goods(self): + if not self.track_semi_finished_goods: + return + + if self.items and not self.transferred_qty and not self.skip_material_transfer: + frappe.throw( + _( + "Materials needs to be transferred to the work in progress warehouse for the job card {0}" + ).format(self.name) + ) + + if self.docstatus == 1 and not self.total_completed_qty: + frappe.throw( + _( + "Total Completed Qty is required for Job Card {0}, please start and complete the job card before submission" + ).format(self.name) + ) def on_update(self): self.validate_job_card_qty() @@ -1354,6 +1373,9 @@ class JobCard(Document): employees=self.employee, sub_operation=kwargs.get("sub_operation"), ) + + if self.docstatus == 1: + self.update_work_order() else: self.add_time_logs(completed_qty=kwargs.qty, employees=self.employee) self.save() diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 78f2ba090cc..a61f95812b1 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -809,7 +809,7 @@ erpnext.work_order = { } } - if (frm.doc.status != "Stopped") { + if (frm.doc.status != "Stopped" && !frm.doc.track_semi_finished_goods) { // If "Material Consumption is check in Manufacturing Settings, allow Material Consumption if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) { if (flt(doc.material_transferred_for_manufacturing) > 0 || frm.doc.skip_transfer) { diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 80cfc0c2a6e..8f8302b4a21 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -248,6 +248,16 @@ class WorkOrder(Document): if self.is_new() and frappe.db.get_single_value("Stock Settings", "auto_reserve_stock"): self.reserve_stock = 1 + def before_save(self): + self.set_skip_transfer_for_operations() + + def set_skip_transfer_for_operations(self): + if not self.track_semi_finished_goods: + return + + for op in self.operations: + op.skip_material_transfer = self.skip_transfer + def validate_operations_sequence(self): if all([not op.sequence_id for op in self.operations]): for op in self.operations: diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 4b7a1e6e27d..2e2639080cf 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2006,6 +2006,7 @@ class StockEntry(StockController, SubcontractingInwardController): else: job_doc.set_consumed_qty_in_job_card_item(self) job_doc.set_manufactured_qty() + job_doc.update_work_order() if self.work_order: pro_doc = frappe.get_doc("Work Order", self.work_order) diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py index 75669657558..30134ad4867 100644 --- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py +++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py @@ -110,7 +110,9 @@ class ManufactureEntry: _dict.from_warehouse = self.source_wh.get(item_code) or self.wip_warehouse _dict.to_warehouse = "" - if backflush_based_on != "BOM": + if backflush_based_on != "BOM" and not frappe.db.get_value( + "Job Card", self.job_card, "skip_material_transfer" + ): calculated_qty = flt(_dict.transferred_qty) - flt(_dict.consumed_qty) if calculated_qty < 0: frappe.throw(