mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-31 10:49:09 +00:00
Merge pull request #29058 from frappe/mergify/bp/version-13-hotfix/pr-28555
fix: Validate Finished Goods for Independent Manufacture entries as well (backport #28555)
This commit is contained in:
@@ -36,10 +36,16 @@ from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle, get
|
|||||||
from erpnext.stock.utils import get_bin, get_incoming_rate
|
from erpnext.stock.utils import get_bin, get_incoming_rate
|
||||||
|
|
||||||
|
|
||||||
class IncorrectValuationRateError(frappe.ValidationError): pass
|
class FinishedGoodError(frappe.ValidationError):
|
||||||
class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass
|
pass
|
||||||
class OperationsNotCompleteError(frappe.ValidationError): pass
|
class IncorrectValuationRateError(frappe.ValidationError):
|
||||||
class MaxSampleAlreadyRetainedError(frappe.ValidationError): pass
|
pass
|
||||||
|
class DuplicateEntryForWorkOrderError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
class OperationsNotCompleteError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
class MaxSampleAlreadyRetainedError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
from erpnext.controllers.stock_controller import StockController
|
from erpnext.controllers.stock_controller import StockController
|
||||||
|
|
||||||
@@ -702,6 +708,11 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
finished_item = self.get_finished_item()
|
finished_item = self.get_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
|
||||||
|
|
||||||
for d in self.items:
|
for d in self.items:
|
||||||
if d.t_warehouse and not d.s_warehouse:
|
if d.t_warehouse and not d.s_warehouse:
|
||||||
if self.purpose=="Repack" or d.item_code == finished_item:
|
if self.purpose=="Repack" or d.item_code == finished_item:
|
||||||
@@ -722,38 +733,64 @@ class StockEntry(StockController):
|
|||||||
return finished_item
|
return finished_item
|
||||||
|
|
||||||
def validate_finished_goods(self):
|
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",
|
wo_details = frappe.db.get_value(
|
||||||
self.work_order, ["production_item", "qty"])
|
"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'):
|
for d in self.get('items'):
|
||||||
if d.is_finished_item:
|
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:
|
if d.item_code != production_item:
|
||||||
frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
|
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):
|
elif flt(d.transfer_qty) > flt(self.fg_completed_qty):
|
||||||
frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \
|
frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}")
|
||||||
format(d.idx, d.transfer_qty, self.fg_completed_qty))
|
.format(d.idx, d.transfer_qty, self.fg_completed_qty)
|
||||||
|
)
|
||||||
|
|
||||||
finished_items.append(d.item_code)
|
finished_items.append(d.item_code)
|
||||||
|
|
||||||
if len(set(finished_items)) > 1:
|
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 self.purpose == "Manufacture":
|
||||||
if not finished_items:
|
if not finished_items:
|
||||||
frappe.throw(_('Finished Good has not set in the stock entry {0}')
|
frappe.throw(
|
||||||
.format(self.name))
|
msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
|
||||||
|
title=_("Missing Finished Good"),
|
||||||
|
exc=FinishedGoodError
|
||||||
|
)
|
||||||
|
|
||||||
allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
|
allowance_percentage = flt(
|
||||||
"overproduction_percentage_for_work_order"))
|
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)
|
# No work order could mean independent Manufacture entry, if so skip validation
|
||||||
if self.fg_completed_qty > allowed_qty:
|
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}")
|
frappe.throw(
|
||||||
.format(flt(self.fg_completed_qty), wo_qty))
|
_("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):
|
def update_stock_ledger(self):
|
||||||
sl_entries = []
|
sl_entries = []
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ from erpnext.stock.doctype.item.test_item import (
|
|||||||
set_item_variant_settings,
|
set_item_variant_settings,
|
||||||
)
|
)
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import * # noqa
|
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 (
|
||||||
|
FinishedGoodError,
|
||||||
|
move_sample_to_retention_warehouse,
|
||||||
|
)
|
||||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
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_ledger_entry.stock_ledger_entry import StockFreezeError
|
||||||
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
|
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
|
||||||
@@ -1007,6 +1010,38 @@ class TestStockEntry(ERPNextTestCase):
|
|||||||
posting_date='2021-09-02', # backdated consumption of 2nd batch
|
posting_date='2021-09-02', # backdated consumption of 2nd batch
|
||||||
purpose='Material Issue')
|
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()
|
||||||
|
|
||||||
def make_serialized_item(**args):
|
def make_serialized_item(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
se = frappe.copy_doc(test_records[0])
|
se = frappe.copy_doc(test_records[0])
|
||||||
|
|||||||
Reference in New Issue
Block a user