From fc57ed1ca84d74d611b9ca9d7c9a209ba0d933f8 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 25 Nov 2021 13:15:25 +0530 Subject: [PATCH 1/7] fix: Validate Finished Goods for Independent Manufacture entries as well - Check if FG exists even if WO is absent - Check if multiple FGs are there, block. (cherry picked from commit d6bc121999352d300dc3aa188d14092a226d8e60) --- .../stock/doctype/stock_entry/stock_entry.py | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 5b27106b825..6b5c0c3f3d2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -722,21 +722,34 @@ class StockEntry(StockController): return finished_item def validate_finished_goods(self): - """validation: finished good quantity should be same as manufacturing quantity""" - if not self.work_order: return + """ + 1. Check if FG exists + 2. Check if Multiple FG Items are present + 3. Check FG Item and Qty against WO if present + """ + production_item, wo_qty, finished_items = None, 0, [] - production_item, wo_qty = frappe.db.get_value("Work Order", - self.work_order, ["production_item", "qty"]) + wo_details = frappe.db.get_value( + "Work Order", self.work_order, ["production_item", "qty"] + ) + if wo_details: + production_item, wo_qty = wo_details - finished_items = [] for d in self.get('items'): if d.is_finished_item: + if not self.work_order: + finished_items.append(d.item_code) + continue # Independent Manufacture Entry, no WO to match against + 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)) + .format(d.item_code, self.work_order) + ) elif flt(d.transfer_qty) > flt(self.fg_completed_qty): - frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \ - format(d.idx, d.transfer_qty, self.fg_completed_qty)) + frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}") + .format(d.idx, d.transfer_qty, self.fg_completed_qty) + ) + finished_items.append(d.item_code) if len(set(finished_items)) > 1: @@ -744,16 +757,24 @@ class StockEntry(StockController): if self.purpose == "Manufacture": if not finished_items: - frappe.throw(_('Finished Good has not set in the stock entry {0}') - .format(self.name)) + frappe.throw( + msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name), + title=_("Missing Finished Good") + ) - allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", - "overproduction_percentage_for_work_order")) + allowance_percentage = flt( + frappe.db.get_single_value( + "Manufacturing Settings","overproduction_percentage_for_work_order" + ) + ) + allowed_qty = wo_qty + ((allowance_percentage/100) * wo_qty) - allowed_qty = wo_qty + (allowance_percentage/100 * wo_qty) - if 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)) + # 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 update_stock_ledger(self): sl_entries = [] From 6c6785e284da1e21687b0e86a76f733029ced472 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 25 Nov 2021 13:23:10 +0530 Subject: [PATCH 2/7] fix: Dont auto set is finished item or is scrap itm checkbox for independent entry - System cant differentiate between scrap and FG when WO is absent, so dont auto set - User must set it manually - `validate_finished_goods` checks this (cherry picked from commit 8c9779d69dca3744fc057aafeecdf2c31f622d60) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 6b5c0c3f3d2..288a62f1620 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -702,6 +702,11 @@ class StockEntry(StockController): finished_item = self.get_finished_item() + if not finished_item: + # In case of independent Manufacture entry, don't auto set + # user must decide and set + return + for d in self.items: if d.t_warehouse and not d.s_warehouse: if self.purpose=="Repack" or d.item_code == finished_item: From d8b781bb1e73fdf1dea6fb6cbc255f7d3dcc8b24 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 26 Nov 2021 13:19:04 +0530 Subject: [PATCH 3/7] test: Independent Manufacture Entry - Check validations - Check if FG basic rate is calculated correctly (cherry picked from commit affb29194bc68a2faf1f16bb40cefc4bc307cec1) # Conflicts: # erpnext/stock/doctype/stock_entry/test_stock_entry.py --- .../stock/doctype/stock_entry/stock_entry.py | 10 ++++-- .../doctype/stock_entry/test_stock_entry.py | 36 ++++++++++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 288a62f1620..90f70a6207f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -36,6 +36,7 @@ from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle, get from erpnext.stock.utils import get_bin, get_incoming_rate +class FinishedGoodError(frappe.ValidationError): pass class IncorrectValuationRateError(frappe.ValidationError): pass class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass class OperationsNotCompleteError(frappe.ValidationError): pass @@ -758,13 +759,18 @@ class StockEntry(StockController): finished_items.append(d.item_code) if len(set(finished_items)) > 1: - frappe.throw(_("Multiple items cannot be marked as finished item")) + frappe.throw( + msg=_("Multiple items cannot be marked as finished item"), + title=_("Note"), + exc=FinishedGoodError + ) if self.purpose == "Manufacture": 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") + title=_("Missing Finished Good"), + exc=FinishedGoodError ) allowance_percentage = flt( diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 642c2636130..df4f5b9d12d 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -16,7 +16,7 @@ from erpnext.stock.doctype.item.test_item import ( set_item_variant_settings, ) from erpnext.stock.doctype.serial_no.serial_no import * # noqa -from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse +from erpnext.stock.doctype.stock_entry.stock_entry import (move_sample_to_retention_warehouse, FinishedGoodError) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( @@ -930,6 +930,7 @@ class TestStockEntry(ERPNextTestCase): distributed_costs = [d.additional_cost for d in se.items] self.assertEqual([40.0, 60.0], distributed_costs) +<<<<<<< HEAD @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_future_negative_sle(self): # Initialize item, batch, warehouse, opening qty @@ -1006,6 +1007,39 @@ class TestStockEntry(ERPNextTestCase): batch_no=batch_nos[1], posting_date='2021-09-02', # backdated consumption of 2nd batch purpose='Material Issue') +======= + def test_independent_manufacture_entry(self): + "Test FG items and incoming rate calculation in Maniufacture Entry without WO or BOM linked." + se = frappe.get_doc( + doctype="Stock Entry", + purpose="Manufacture", + stock_entry_type="Manufacture", + company="_Test Company", + items=[ + frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"), + frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC") + ] + ) + # SE must have atleast one FG + self.assertRaises(FinishedGoodError, se.save) + + se.items[0].is_finished_item = 1 + se.items[1].is_finished_item = 1 + # SE cannot have multiple FGs + self.assertRaises(FinishedGoodError, se.save) + + se.items[0].is_finished_item = 0 + se.save() + + # Check if FG cost is calculated based on RM total cost + # RM total cost = 200, FG rate = 200/4(FG qty) = 50 + self.assertEqual(se.items[1].basic_rate, 50) + self.assertEqual(se.value_difference, 0.0) + self.assertEqual(se.total_incoming_value, se.total_outgoing_value) + + # teardown + se.delete() +>>>>>>> affb29194b (test: Independent Manufacture Entry) def make_serialized_item(**args): args = frappe._dict(args) From 66a5b3f34d362cca8a578d8af85f52bdc701c8ba Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 26 Nov 2021 13:25:23 +0530 Subject: [PATCH 4/7] fix: Linter (imports) (cherry picked from commit dfff972a78e802e3364548bf6378361e51a4c46a) --- erpnext/stock/doctype/stock_entry/test_stock_entry.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index df4f5b9d12d..82e4577d8c6 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -16,7 +16,10 @@ from erpnext.stock.doctype.item.test_item import ( set_item_variant_settings, ) from erpnext.stock.doctype.serial_no.serial_no import * # noqa -from erpnext.stock.doctype.stock_entry.stock_entry import (move_sample_to_retention_warehouse, FinishedGoodError) +from erpnext.stock.doctype.stock_entry.stock_entry import ( + FinishedGoodError, + move_sample_to_retention_warehouse, +) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( From fdf5811c27647a6e5aa14a14b798a5b3b7a73088 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 28 Dec 2021 13:06:35 +0530 Subject: [PATCH 5/7] fix: Sider (cherry picked from commit 7d0340e660780e3e678e110ef36b9afc2b51c98e) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 90f70a6207f..894640f4819 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -36,11 +36,16 @@ from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle, get from erpnext.stock.utils import get_bin, get_incoming_rate -class FinishedGoodError(frappe.ValidationError): pass -class IncorrectValuationRateError(frappe.ValidationError): pass -class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass -class OperationsNotCompleteError(frappe.ValidationError): pass -class MaxSampleAlreadyRetainedError(frappe.ValidationError): pass +class FinishedGoodError(frappe.ValidationError): + pass +class IncorrectValuationRateError(frappe.ValidationError): + pass +class DuplicateEntryForWorkOrderError(frappe.ValidationError): + pass +class OperationsNotCompleteError(frappe.ValidationError): + pass +class MaxSampleAlreadyRetainedError(frappe.ValidationError): + pass from erpnext.controllers.stock_controller import StockController From 59aaef371956d40436e37da5e0252acf22b312b6 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 28 Dec 2021 13:23:27 +0530 Subject: [PATCH 6/7] fix: Avoid Impact on Repack Entry - Repack entries must be considered for fg/scrap checkbox auto set - This must be avoided only for Manufacture entries, due to confusion between scrap and fg - No scrap in repack, so its straight forward (cherry picked from commit 22809a28381ac92336b1fbdb0b1caca2c7b3de7e) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 894640f4819..b4865237100 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -708,7 +708,7 @@ class StockEntry(StockController): finished_item = self.get_finished_item() - if not finished_item: + if not finished_item and self.purpose == "Manufacture": # In case of independent Manufacture entry, don't auto set # user must decide and set return From 00b9f1fbef31c4fc860567a01bf39fa3a9ee9bbe Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 28 Dec 2021 14:41:03 +0530 Subject: [PATCH 7/7] fix: Merge conflict --- erpnext/stock/doctype/stock_entry/test_stock_entry.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 82e4577d8c6..1e0335da73d 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -933,7 +933,6 @@ class TestStockEntry(ERPNextTestCase): distributed_costs = [d.additional_cost for d in se.items] self.assertEqual([40.0, 60.0], distributed_costs) -<<<<<<< HEAD @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_future_negative_sle(self): # Initialize item, batch, warehouse, opening qty @@ -1010,7 +1009,7 @@ class TestStockEntry(ERPNextTestCase): batch_no=batch_nos[1], posting_date='2021-09-02', # backdated consumption of 2nd batch purpose='Material Issue') -======= + def test_independent_manufacture_entry(self): "Test FG items and incoming rate calculation in Maniufacture Entry without WO or BOM linked." se = frappe.get_doc( @@ -1042,7 +1041,6 @@ class TestStockEntry(ERPNextTestCase): # teardown se.delete() ->>>>>>> affb29194b (test: Independent Manufacture Entry) def make_serialized_item(**args): args = frappe._dict(args)