From baeb4690d42429a062595eb9058ddd2770b190c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Rold=C3=A1n?= Date: Thu, 1 Feb 2024 17:37:08 -0300 Subject: [PATCH] feat: manufacturing over/under production * feat: WIP Manufacturing Over/Under Production * feat: WIP Manufacturing Over/Under Production * feat: WIP Manufacturing Over/Under Production * fix: unused import * feat: override onload for work order * feat: oveeride get_pending_raw_materials * fix: allowed_qty in job card * fix: indentation * fix: import * fixes * wip: tests * feat: test_get_allowance_percentage * feat: test test_validate_finished_goods * fix: validate_job_card and test * fix: test --- inventory_tools/hooks.py | 2 + .../inventory_tools/custom/bom.json | 63 ++++++- .../inventory_tools_settings.json | 13 +- .../inventory_tools/overrides/job_card.py | 42 +++++ .../inventory_tools/overrides/stock_entry.py | 153 ++++++++++++++++ .../inventory_tools/overrides/work_order.py | 117 +++++++++++- .../public/js/stock_entry_custom.js | 0 inventory_tools/tests/fixtures.py | 1 + inventory_tools/tests/setup.py | 6 + inventory_tools/tests/test_overproduction.py | 168 ++++++++++++++++++ 10 files changed, 562 insertions(+), 3 deletions(-) create mode 100644 inventory_tools/inventory_tools/overrides/job_card.py create mode 100644 inventory_tools/inventory_tools/overrides/stock_entry.py create mode 100644 inventory_tools/public/js/stock_entry_custom.js create mode 100644 inventory_tools/tests/test_overproduction.py diff --git a/inventory_tools/hooks.py b/inventory_tools/hooks.py index fba8d44..e68bd6c 100644 --- a/inventory_tools/hooks.py +++ b/inventory_tools/hooks.py @@ -111,6 +111,8 @@ "Purchase Order": "inventory_tools.inventory_tools.overrides.purchase_order.InventoryToolsPurchaseOrder", "Purchase Receipt": "inventory_tools.inventory_tools.overrides.purchase_receipt.InventoryToolsPurchaseReceipt", "Production Plan": "inventory_tools.inventory_tools.overrides.production_plan.InventoryToolsProductionPlan", + "Stock Entry": "inventory_tools.inventory_tools.overrides.stock_entry.InventoryToolsStockEntry", + "Job Card": "inventory_tools.inventory_tools.overrides.job_card.InventoryToolsJobCard", } diff --git a/inventory_tools/inventory_tools/custom/bom.json b/inventory_tools/inventory_tools/custom/bom.json index 95f85f0..4f7122f 100644 --- a/inventory_tools/inventory_tools/custom/bom.json +++ b/inventory_tools/inventory_tools/custom/bom.json @@ -86,7 +86,7 @@ "hide_border": 0, "hide_days": 0, "hide_seconds": 0, - "idx": 11, + "idx": 12, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_global_search": 0, @@ -121,6 +121,67 @@ "translatable": 0, "unique": 0, "width": null + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "creation": "2024-01-11 10:05:40.558053", + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "dt": "BOM", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "overproduction_percentage_for_work_order", + "fieldtype": "Percent", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "idx": 12, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "create_job_cards_automatically", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Overproduction Percentage For Work Order", + "length": 0, + "mandatory_depends_on": null, + "modified": "2024-01-11 10:07:40.558053", + "modified_by": "Administrator", + "module": "Inventory Tools", + "name": "BOM-overproduction_percentage_for_work_order", + "no_copy": 0, + "non_negative": 0, + "options": null, + "owner": "Administrator", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0, + "width": null } ], "custom_perms": [], diff --git a/inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.json b/inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.json index 5c84509..ec87336 100644 --- a/inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.json +++ b/inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.json @@ -17,6 +17,8 @@ "create_purchase_orders", "bom_column", "create_job_cards_automatically", + "column_break_ilobm", + "overproduction_percentage_for_work_order", "section_break_0", "update_warehouse_path", "section_break_gzcbr", @@ -108,11 +110,20 @@ "fieldtype": "Select", "label": "Create Job Card(s) Automatically", "options": "Yes\nNo" + }, + { + "fieldname": "column_break_ilobm", + "fieldtype": "Column Break" + }, + { + "fieldname": "overproduction_percentage_for_work_order", + "fieldtype": "Percent", + "label": "Overproduction Percentage For Work Order" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-01-09 18:12:20.714002", + "modified": "2024-01-11 10:10:57.356068", "modified_by": "Administrator", "module": "Inventory Tools", "name": "Inventory Tools Settings", diff --git a/inventory_tools/inventory_tools/overrides/job_card.py b/inventory_tools/inventory_tools/overrides/job_card.py new file mode 100644 index 0000000..f86de01 --- /dev/null +++ b/inventory_tools/inventory_tools/overrides/job_card.py @@ -0,0 +1,42 @@ +import frappe +from erpnext.manufacturing.doctype.job_card.job_card import JobCard +from frappe import _, bold +from frappe.utils import get_link_to_form + +from inventory_tools.inventory_tools.overrides.work_order import get_allowance_percentage + + +class InventoryToolsJobCard(JobCard): + def validate_job_card(self): + if ( + self.work_order + and frappe.get_cached_value("Work Order", self.work_order, "status") == "Stopped" + ): + frappe.throw( + _("Transaction not allowed against stopped Work Order {0}").format( + get_link_to_form("Work Order", self.work_order) + ) + ) + + if not self.time_logs: + frappe.throw( + _("Time logs are required for {0} {1}").format( + bold("Job Card"), get_link_to_form("Job Card", self.name) + ) + ) + + # don't validate mfg qty so partial consumption can take place + # PATCH: use manufacturing settings overproduction percentage to allow overproduction on Job Card + overproduction_percentage = get_allowance_percentage(self.company, self.bom_no) + allowed_qty = self.for_quantity * (1 + overproduction_percentage / 100) + if self.for_quantity and self.total_completed_qty > allowed_qty: + total_completed_qty = frappe.bold(frappe._("Total Completed Qty")) + qty_to_manufacture = frappe.bold(frappe._("Qty to Manufacture")) + frappe.throw( + frappe._("The {0} ({1}) must be equal to {2} ({3})").format( + total_completed_qty, + frappe.bold(self.total_completed_qty), + qty_to_manufacture, + frappe.bold(self.for_quantity), + ) + ) diff --git a/inventory_tools/inventory_tools/overrides/stock_entry.py b/inventory_tools/inventory_tools/overrides/stock_entry.py new file mode 100644 index 0000000..09bc0be --- /dev/null +++ b/inventory_tools/inventory_tools/overrides/stock_entry.py @@ -0,0 +1,153 @@ +import frappe +from erpnext.stock.doctype.stock_entry.stock_entry import FinishedGoodError, StockEntry +from frappe import _ +from frappe.utils import flt + +from inventory_tools.inventory_tools.overrides.work_order import get_allowance_percentage + + +class InventoryToolsStockEntry(StockEntry): + def check_if_operations_completed(self): + """ + Original code checks that the stock entry amount plus what's already produced in the WO + is not larger than any operation's completed quantity (plus the overallowance amount). + Since customized code rewires so stock entries happen via a Job Card, the function now + checks that the stock entry amount plus what's already been produced in the WO is not + greater than the amount to be manufactured plus the overallowance amount. + """ + prod_order = frappe.get_doc("Work Order", self.work_order) + allowance_percentage = get_allowance_percentage(self.company, self.bom_no) + + jc_qty = flt( + self.fg_completed_qty + ) # quantity manufactured and being entered in stock entry for this JC + already_produced = flt(prod_order.produced_qty) # quantity already manufactured for WO + total_completed_qty = jc_qty + already_produced + + wo_to_man_qty = flt(prod_order.qty) + allowed_qty = wo_to_man_qty * ( + 1 + allowance_percentage / 100 + ) # amount to be manufactured on the WO including the overallowance amount + + if total_completed_qty > allowed_qty: + work_order_link = frappe.utils.get_link_to_form("Work Order", self.work_order) + frappe.throw( + _( + "Quantity manufactured in this Job Card of {0} plus quantity already produced for Work Order {1} of {2} is greater than the Work Order's quantity to manufacture of {3} plus the overproduction allowance of {4}%" + ).format( + self.fg_completed_qty, + work_order_link, + already_produced, + wo_to_man_qty, + allowance_percentage, + ) + ) + + def validate_finished_goods(self): + """ + 1. Check if FG exists (mfg, repack) + 2. Check if Multiple FG Items are present (mfg) + 3. Check FG Item and Qty against WO if present (mfg) + """ + production_item, wo_qty, finished_items = None, 0, [] + + wo_details = frappe.db.get_value("Work Order", self.work_order, ["production_item", "qty"]) + if wo_details: + production_item, wo_qty = wo_details + + for d in self.get("items"): + if d.is_finished_item: + if not self.work_order: + # Independent MFG Entry/ Repack Entry, no WO to match against + finished_items.append(d.item_code) + continue + + if d.item_code != production_item: + frappe.throw( + _("Finished Item {0} does not match with Work Order {1}").format( + d.item_code, self.work_order + ) + ) + elif flt(d.qty) > flt(self.fg_completed_qty): + frappe.throw( + _("Quantity in row {0} ({1}) must be same as manufactured quantity {2}").format( + d.idx, d.qty, self.fg_completed_qty + ) + ) + + finished_items.append(d.item_code) + + if not finished_items: + frappe.throw( + msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name), + title=_("Missing Finished Good"), + exc=FinishedGoodError, + ) + + if self.purpose == "Manufacture": + if len(set(finished_items)) > 1: + frappe.throw( + msg=_("Multiple items cannot be marked as finished item"), + title=_("Note"), + exc=FinishedGoodError, + ) + + allowance_percentage = get_allowance_percentage(self.company, self.bom_no) + allowed_qty = wo_qty + ((allowance_percentage / 100) * wo_qty) + + # No work order could mean independent Manufacture entry, if so skip validation + if self.work_order and self.fg_completed_qty > allowed_qty: + frappe.throw( + _("For quantity {0} should not be greater than work order quantity {1}").format( + flt(self.fg_completed_qty), wo_qty + ) + ) + + def get_pending_raw_materials(self, backflush_based_on=None): + """ + issue (item quantity) that is pending to issue or desire to transfer, + whichever is less + """ + item_dict = self.get_pro_order_required_items(backflush_based_on) + + max_qty = flt(self.pro_doc.qty) + + allow_overproduction = False + overproduction_percentage = get_allowance_percentage(self.company, self.bom_no) + + to_transfer_qty = flt(self.pro_doc.material_transferred_for_manufacturing) + flt( + self.fg_completed_qty + ) + transfer_limit_qty = max_qty + ((max_qty * overproduction_percentage) / 100) + + if transfer_limit_qty >= to_transfer_qty: + allow_overproduction = True + + for item, item_details in item_dict.items(): + pending_to_issue = flt(item_details.required_qty) - flt(item_details.transferred_qty) + desire_to_transfer = flt(self.fg_completed_qty) * flt(item_details.required_qty) / max_qty + + if ( + desire_to_transfer <= pending_to_issue + or (desire_to_transfer > 0 and backflush_based_on == "Material Transferred for Manufacture") + or allow_overproduction + ): + # "No need for transfer but qty still pending to transfer" case can occur + # when transferring multiple RM in different Stock Entries + item_dict[item]["qty"] = desire_to_transfer if (desire_to_transfer > 0) else pending_to_issue + elif pending_to_issue > 0: + item_dict[item]["qty"] = pending_to_issue + else: + item_dict[item]["qty"] = 0 + + # delete items with 0 qty + list_of_items = list(item_dict.keys()) + for item in list_of_items: + if not item_dict[item]["qty"]: + del item_dict[item] + + # show some message + if not len(item_dict): + frappe.msgprint(_("""All items have already been transferred for this Work Order.""")) + + return item_dict diff --git a/inventory_tools/inventory_tools/overrides/work_order.py b/inventory_tools/inventory_tools/overrides/work_order.py index 92f7a52..3b93170 100644 --- a/inventory_tools/inventory_tools/overrides/work_order.py +++ b/inventory_tools/inventory_tools/overrides/work_order.py @@ -1,13 +1,24 @@ import frappe from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_children -from erpnext.manufacturing.doctype.work_order.work_order import WorkOrder +from erpnext.manufacturing.doctype.work_order.work_order import ( + OverProductionError, + StockOverProductionError, + WorkOrder, +) from erpnext.manufacturing.doctype.work_order.work_order import ( make_stock_entry as _make_stock_entry, ) +from frappe import _ from frappe.utils import flt, get_link_to_form, getdate, nowdate class InventoryToolsWorkOrder(WorkOrder): + def onload(self): + ms = frappe.get_doc("Manufacturing Settings") + self.set_onload("material_consumption", ms.material_consumption) + self.set_onload("backflush_raw_materials_based_on", ms.backflush_raw_materials_based_on) + self.set_onload("overproduction_percentage", get_allowance_percentage(self.company, self.bom_no)) + def validate(self): if self.is_work_order_subcontracting_enabled() and frappe.get_value( "BOM", self.bom_no, "is_subcontracted" @@ -91,6 +102,93 @@ def create_job_card(self): return return super().create_job_card() + def update_work_order_qty(self): + """Update **Manufactured Qty** and **Material Transferred for Qty** in Work Order + based on Stock Entry""" + allowance_percentage = get_allowance_percentage(self.company, self.bom_no) + + for purpose, fieldname in ( + ("Manufacture", "produced_qty"), + ("Material Transfer for Manufacture", "material_transferred_for_manufacturing"), + ): + if ( + purpose == "Material Transfer for Manufacture" + and self.operations + and self.transfer_material_against == "Job Card" + ): + continue + + qty = self.get_transferred_or_manufactured_qty(purpose) + + completed_qty = self.qty + (allowance_percentage / 100 * self.qty) + if qty > completed_qty: + frappe.throw( + _("{0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3}").format( + self.meta.get_label(fieldname), qty, completed_qty, self.name + ), + StockOverProductionError, + ) + + self.db_set(fieldname, qty) + self.set_process_loss_qty() + + from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item + + if self.sales_order and self.sales_order_item: + update_produced_qty_in_so_item(self.sales_order, self.sales_order_item) + + if self.production_plan: + self.update_production_plan_status() + + def update_operation_status(self): + allowance_percentage = get_allowance_percentage(self.company, self.bom_no) + max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage / 100 * flt(self.qty)) + + for d in self.get("operations"): + if not d.completed_qty: + d.status = "Pending" + elif flt(d.completed_qty) < flt(self.qty): + d.status = "Work in Progress" + elif flt(d.completed_qty) == flt(self.qty): + d.status = "Completed" + elif flt(d.completed_qty) <= max_allowed_qty_for_wo: + d.status = "Completed" + else: + frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'")) + + def validate_qty(self): + + if not self.qty > 0: + frappe.throw(_("Quantity to Manufacture must be greater than 0.")) + + if ( + self.production_plan + and self.production_plan_item + and not self.production_plan_sub_assembly_item + ): + qty_dict = frappe.db.get_value( + "Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1 + ) + + if not qty_dict: + return + + allowance_qty = ( + get_allowance_percentage(self.company, self.bom_no) / 100 * qty_dict.get("planned_qty", 0) + ) + + max_qty = qty_dict.get("planned_qty", 0) + allowance_qty - qty_dict.get("ordered_qty", 0) + + if not max_qty > 0: + frappe.throw( + _("Cannot produce more item for {0}").format(self.production_item), OverProductionError + ) + elif self.qty > max_qty: + frappe.throw( + _("Cannot produce more than {0} items for {1}").format(max_qty, self.production_item), + OverProductionError, + ) + @frappe.whitelist() def make_subcontracted_purchase_order(wo_name, supplier=None): @@ -380,3 +478,20 @@ def make_stock_entry(work_order_id, purpose, qty=None): row["s_warehouse"] = None row["t_warehouse"] = return_warehouse return se + + +def get_allowance_percentage(company: str, bom_no: str): + bom_allowance_percentage = frappe.get_value( + "BOM", bom_no, "overproduction_percentage_for_work_order" + ) + if bom_allowance_percentage: + return flt(bom_allowance_percentage) + + settings = frappe.get_doc("Inventory Tools Settings", {"company": company}) + if settings: + settings_allowance_percentage = flt(settings.overproduction_percentage_for_work_order) + else: + settings_allowance_percentage = flt( + frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") + ) + return settings_allowance_percentage diff --git a/inventory_tools/public/js/stock_entry_custom.js b/inventory_tools/public/js/stock_entry_custom.js new file mode 100644 index 0000000..e69de29 diff --git a/inventory_tools/tests/fixtures.py b/inventory_tools/tests/fixtures.py index 63d0103..d8fc8a2 100644 --- a/inventory_tools/tests/fixtures.py +++ b/inventory_tools/tests/fixtures.py @@ -527,6 +527,7 @@ "item": "Ambrosia Pie", "quantity": 5.0, "uom": "Nos", + "overproduction_percentage_for_work_order": 100, "items": [ {"item_code": "Pie Crust", "qty": 5.0, "qty_consumed_per_unit": 1.0, "uom": "Nos"}, {"item_code": "Ambrosia Pie Filling", "qty": 20.0, "qty_consumed_per_unit": 4.0, "uom": "Cup"}, diff --git a/inventory_tools/tests/setup.py b/inventory_tools/tests/setup.py index d98dcf2..79322ec 100644 --- a/inventory_tools/tests/setup.py +++ b/inventory_tools/tests/setup.py @@ -178,6 +178,9 @@ def setup_manufacturing_settings(settings): "Inventory Tools Settings", settings.company, "enable_work_order_subcontracting", 1 ) frappe.set_value("Inventory Tools Settings", settings.company, "create_purchase_orders", 0) + frappe.set_value( + "Inventory Tools Settings", settings.company, "overproduction_percentage_for_work_order", 50 + ) def create_workstations(): @@ -374,6 +377,9 @@ def create_boms(settings): b.company = settings.company b.is_default = 0 if bom.get("is_default") == 0 else 1 b.is_subcontracted = bom.get("is_subcontracted") or 0 + b.overproduction_percentage_for_work_order = bom.get( + "overproduction_percentage_for_work_order", None + ) b.rm_cost_as_per = "Price List" b.buying_price_list = "Bakery Buying" b.currency = "USD" diff --git a/inventory_tools/tests/test_overproduction.py b/inventory_tools/tests/test_overproduction.py new file mode 100644 index 0000000..cbfb904 --- /dev/null +++ b/inventory_tools/tests/test_overproduction.py @@ -0,0 +1,168 @@ +import frappe +import pytest +from erpnext.manufacturing.doctype.work_order.work_order import ( + create_job_card, + make_stock_entry, + make_work_order, +) +from frappe.exceptions import ValidationError +from frappe.utils import now + +from inventory_tools.inventory_tools.overrides.work_order import get_allowance_percentage + + +def test_get_allowance_percentage(): + work_order = frappe.get_doc("Work Order", {"item_name": "Gooseberry Pie"}) + bom = frappe.get_doc("BOM", work_order.bom_no) + + inventory_tools_settings = frappe.get_doc( + "Inventory Tools Settings", {"company": work_order.company} + ) + # No value set + inventory_tools_settings.overproduction_percentage_for_work_order = 0.00 + inventory_tools_settings.save() + bom.overproduction_percentage_for_work_order = 0.0 + bom.save() + assert get_allowance_percentage(work_order.company, bom.name) == 0.0 + + # Uses value from inventory tools settings + inventory_tools_settings.overproduction_percentage_for_work_order = 50.0 + inventory_tools_settings.save() + bom.overproduction_percentage_for_work_order = 0.0 + bom.save() + assert get_allowance_percentage(work_order.company, bom.name) == 50.0 + + # Uses value from BOM + inventory_tools_settings.overproduction_percentage_for_work_order = 50.0 + inventory_tools_settings.save() + bom.overproduction_percentage_for_work_order = 100.0 + bom.save() + assert get_allowance_percentage(work_order.company, bom.name) == 100.0 + + +def test_check_if_operations_completed(): + + # BOM with overproduction_percentage_for_work_order configured + work_order = frappe.get_doc("Work Order", {"item_name": "Ambrosia Pie"}) + se = make_stock_entry( + work_order_id=work_order.name, purpose="Material Transfer for Manufacture", qty=work_order.qty + ) + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.update(se) + + assert stock_entry.check_if_operations_completed() is None + + overproduction_percentage_for_work_order = frappe.db.get_value( + "BOM", stock_entry.bom_no, "overproduction_percentage_for_work_order" + ) + qty = work_order.qty * (1 + overproduction_percentage_for_work_order / 100) + se = make_stock_entry( + work_order_id=work_order.name, purpose="Material Transfer for Manufacture", qty=qty + ) + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.update(se) + assert stock_entry.check_if_operations_completed() is None + + with pytest.raises(ValidationError) as exc_info: + qty = qty + 1 + se = make_stock_entry( + work_order_id=work_order.name, purpose="Material Transfer for Manufacture", qty=qty + ) + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.update(se) + stock_entry.check_if_operations_completed() + + assert ( + f"is greater than the Work Order's quantity to manufacture of {work_order.qty} plus the overproduction allowance of {overproduction_percentage_for_work_order}%" + in exc_info.value.args[0] + ) + + # BOM without overproduction_percentage_for_work_order configured + work_order = frappe.get_doc("Work Order", {"item_name": "Double Plum Pie"}) + overproduction_percentage_for_work_order = frappe.db.get_value( + "BOM", work_order.bom_no, "overproduction_percentage_for_work_order" + ) + assert overproduction_percentage_for_work_order == 0.0 + + overproduction_percentage_for_work_order = frappe.get_value( + "Inventory Tools Settings", work_order.company, "overproduction_percentage_for_work_order" + ) + assert overproduction_percentage_for_work_order != 0.0 + + qty = work_order.qty * (1 + overproduction_percentage_for_work_order / 100) + se = make_stock_entry( + work_order_id=work_order.name, purpose="Material Transfer for Manufacture", qty=qty + ) + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.update(se) + assert stock_entry.check_if_operations_completed() is None + + with pytest.raises(ValidationError) as exc_info: + qty = qty + 1 + se = make_stock_entry( + work_order_id=work_order.name, purpose="Material Transfer for Manufacture", qty=qty + ) + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.update(se) + stock_entry.check_if_operations_completed() + + assert ( + f"is greater than the Work Order's quantity to manufacture of {work_order.qty} plus the overproduction allowance of {overproduction_percentage_for_work_order}%" + in exc_info.value.args[0] + ) + + +def test_validate_finished_goods(): + work_order = frappe.get_doc("Work Order", {"item_name": "Ambrosia Pie"}) + se = make_stock_entry(work_order_id=work_order.name, purpose="Manufacture", qty=work_order.qty) + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.update(se) + assert stock_entry.validate_finished_goods() is None + + with pytest.raises(ValidationError) as exc_info: + stock_entry.fg_completed_qty = work_order.qty * 10 + stock_entry.validate_finished_goods() + + assert ( + f"For quantity {work_order.qty * 10} should not be greater than work order quantity {work_order.qty}" + in exc_info.value.args[0] + ) + + +def test_validate_job_card(): + work_order = frappe.get_doc("Work Order", {"item_name": "Ambrosia Pie"}) + jc = frappe.get_doc( + "Job Card", {"work_order": work_order.name, "operation": work_order.operations[0].operation} + ) + jc.cancel() + job_card = create_job_card(work_order, work_order.operations[0].as_dict(), auto_create=True) + job_card.append( + "time_logs", + { + "from_time": now(), + "to_time": now(), + "completed_qty": work_order.qty, + }, + ) + job_card.save() + assert job_card.validate_job_card() == None + + overproduction_percentage_for_work_order = frappe.db.get_value( + "BOM", work_order.bom_no, "overproduction_percentage_for_work_order" + ) + over_production_qty = work_order.qty * (1 + overproduction_percentage_for_work_order / 100) + job_card.time_logs[0].completed_qty = over_production_qty + job_card.save() + + assert job_card.validate_job_card() == None + + job_card.time_logs[0].completed_qty = over_production_qty + 10 + job_card.save() + + with pytest.raises(ValidationError) as exc_info: + job_card.validate_job_card() + + assert ( + f"The Total Completed Qty ({over_production_qty + 10}) must be equal to Qty to Manufacture ({job_card.for_quantity})" + in exc_info.value.args[0] + )