From e5fb23972ab1e482404bdb5f0a8c919aa209c265 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 18 Jun 2021 20:37:42 +0530 Subject: [PATCH] fix: available qty for consumption --- .../purchase_order/test_purchase_order.py | 3 - .../purchase_receipt_item_supplied.json | 20 ++++-- erpnext/controllers/buying_controller.py | 10 +-- erpnext/controllers/subcontracting.py | 66 ++++++++++++++++--- erpnext/stock/stock_ledger.py | 2 +- erpnext/tests/test_subcontracting.py | 31 +++++++++ 6 files changed, 110 insertions(+), 22 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 33d1971451..8563b97ab7 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -847,9 +847,6 @@ class TestPurchaseOrder(unittest.TestCase): for item in rm_items: transferred_rm_map[item.get('rm_item_code')] = item - for item in pr.get('supplied_items'): - self.assertEqual(item.get('required_qty'), (transferred_rm_map[item.get('rm_item_code')].get('qty') / order_qty) * received_qty) - update_backflush_based_on("BOM") def test_supplied_qty_against_subcontracted_po(self): diff --git a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json index d8c37f5881..f9cd72015a 100644 --- a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json +++ b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json @@ -26,7 +26,8 @@ "secbreak_3", "batch_no", "col_break4", - "serial_no" + "serial_no", + "purchase_order" ], "fields": [ { @@ -81,9 +82,10 @@ "fieldname": "required_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Required Qty", + "label": "Available Qty For Consumption", "oldfieldname": "required_qty", "oldfieldtype": "Currency", + "print_hide": 1, "read_only": 1 }, { @@ -91,7 +93,7 @@ "fieldname": "consumed_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Consumed Qty", + "label": "Qty to Be Consumed", "oldfieldname": "consumed_qty", "oldfieldtype": "Currency", "reqd": 1 @@ -190,12 +192,22 @@ "fieldtype": "Data", "label": "Item Name", "read_only": 1 + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "hidden": 1, + "label": "Purchase Order", + "no_copy": 1, + "options": "Purchase Order", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-05-29 17:22:14.977117", + "modified": "2021-06-19 19:33:04.431213", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Receipt Item Supplied", diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 0b0da5f413..6a550e0e97 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -292,11 +292,13 @@ class BuyingController(StockController, Subcontracting): if item in self.sub_contracted_items and not item.bom: frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code)) - if self.doctype == "Purchase Order": - for supplied_item in self.get("supplied_items"): - if not supplied_item.reserve_warehouse: - frappe.throw(_("Reserved Warehouse is mandatory for Item {0} in Raw Materials supplied").format(frappe.bold(supplied_item.rm_item_code))) + if self.doctype != "Purchase Order": + return + for row in self.get("supplied_items"): + if not row.reserve_warehouse: + msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied" + frappe.throw(_(msg)) else: for item in self.get("items"): if item.bom: diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py index db841626a5..36ae110216 100644 --- a/erpnext/controllers/subcontracting.py +++ b/erpnext/controllers/subcontracting.py @@ -1,6 +1,7 @@ import frappe +import copy from frappe import _ -from frappe.utils import flt, cint +from frappe.utils import flt, cint, get_link_to_form from collections import defaultdict from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -12,7 +13,7 @@ class Subcontracting(): self.raw_material_table = raw_material_table self.__identify_change_in_item_table() self.__prepare_supplied_items() - self.__validate_consumed_qty() + self.__validate_supplied_items() def __prepare_supplied_items(self): self.initialized_fields() @@ -24,6 +25,7 @@ class Subcontracting(): def initialized_fields(self): self.available_materials = frappe._dict() + self.__transferred_items = frappe._dict() self.alternative_item_details = frappe._dict() self.__get_backflush_based_on() @@ -100,6 +102,7 @@ class Subcontracting(): self.__set_alternative_item_details(row) + self.__transferred_items = copy.deepcopy(self.available_materials) for doctype in ['Purchase Receipt', 'Purchase Invoice']: self.__update_consumed_materials(doctype) @@ -254,6 +257,8 @@ class Subcontracting(): if self.qty_to_be_received: qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0)) + transfer_item.item_details.required_qty = transfer_item.qty + if (transfer_item.serial_no or frappe.get_cached_value('UOM', transfer_item.item_details.stock_uom, 'must_be_whole_number')): return frappe.utils.ceil(qty) @@ -272,12 +277,15 @@ class Subcontracting(): if self.doctype == 'Purchase Order': rm_obj.required_qty = qty else: + rm_obj.consumed_qty = 0 + rm_obj.purchase_order = item_row.purchase_order self.__set_batch_nos(bom_item, item_row, rm_obj, qty) def __set_batch_nos(self, bom_item, item_row, rm_obj, qty): key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order) if (self.available_materials.get(key) and self.available_materials[key]['batch_no']): + new_rm_obj = None for batch_no, batch_qty in self.available_materials[key]['batch_no'].items(): if batch_qty >= qty: self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) @@ -290,13 +298,21 @@ class Subcontracting(): new_rm_obj.reference_name = item_row.name self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty) self.available_materials[key]['batch_no'][batch_no] = 0 + + if abs(qty) > 0 and not new_rm_obj: + self.__set_consumed_qty(rm_obj, qty) else: - rm_obj.required_qty = qty - rm_obj.consumed_qty = qty + self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty) self.__set_serial_nos(item_row, rm_obj) + def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0): + rm_obj.required_qty = required_qty + rm_obj.consumed_qty = consumed_qty + def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty): - rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no, 'required_qty': qty}) + rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no, + 'required_qty': qty, 'purchase_order': item_row.purchase_order}) + self.__set_serial_nos(item_row, rm_obj) def __set_serial_nos(self, item_row, rm_obj): @@ -339,9 +355,39 @@ class Subcontracting(): itemwise_consumed_qty[key] -= consumed_qty frappe.db.set_value('Purchase Order Item Supplied', row.name, 'consumed_qty', consumed_qty) - def __validate_consumed_qty(self): - for row in self.get(self.raw_material_table): - if flt(row.consumed_qty) == 0.0 and row.get('serial_no'): - msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}' + def __validate_supplied_items(self): + if self.doctype not in ['Purchase Invoice', 'Purchase Receipt']: + return - frappe.throw(_(msg),title=_('Consumed Items Qty Check')) \ No newline at end of file + for row in self.get(self.raw_material_table): + self.__validate_consumed_qty(row) + + key = (row.rm_item_code, row.main_item_code, row.purchase_order) + if not self.__transferred_items or not self.__transferred_items.get(key): + return + + self.__validate_batch_no(row, key) + self.__validate_serial_no(row, key) + + def __validate_consumed_qty(self, row): + if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0: + msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}' + + frappe.throw(_(msg),title=_('Consumed Items Qty Check')) + + def __validate_batch_no(self, row, key): + if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'): + link = get_link_to_form('Purchase Order', row.purchase_order) + msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Purchase Order {link}' + frappe.throw(_(msg), title=_("Incorrect Batch Consumed")) + + def __validate_serial_no(self, row, key): + if row.get('serial_no'): + serial_nos = get_serial_nos(row.get('serial_no')) + incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get('serial_no')) + + if incorrect_sn: + incorrect_sn = "\n".join(incorrect_sn) + link = get_link_to_form('Purchase Order', row.purchase_order) + msg = f'The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}' + frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed")) \ No newline at end of file diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index fb2ecab249..9fe89c3fa5 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -485,7 +485,7 @@ class update_entries_after(object): # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == 'Yes': - doc = frappe.get_cached_doc(sle.voucher_type, sle.voucher_no) + doc = frappe.get_doc(sle.voucher_type, sle.voucher_no) doc.update_valuation_rate(reset_outgoing_rate=False) for d in (doc.items + doc.supplied_items): d.db_update() diff --git a/erpnext/tests/test_subcontracting.py b/erpnext/tests/test_subcontracting.py index d2438f8c60..8b0ce0957d 100644 --- a/erpnext/tests/test_subcontracting.py +++ b/erpnext/tests/test_subcontracting.py @@ -395,6 +395,37 @@ class TestSubcontracting(unittest.TestCase): self.assertEqual(value.qty, details.qty) self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) + def test_incorrect_serial_no_components_based_on_material_transfer(self): + ''' + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA2. + - Transfer the serialized componenets to the supplier. + - Create purchase receipt and change the serial no which is not transferred. + - System should throw the error and not allowed to save the purchase receipt. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + item_code = 'Subcontracted Item SA2' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 10}] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.save() + pr1.supplied_items[0].serial_no = 'ABCD' + self.assertRaises(frappe.ValidationError, pr1.save) + pr1.delete() + def test_partial_transfer_batch_based_on_material_transfer(self): ''' - Set backflush based on Material Transferred for Subcontract