diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 0cf3d24478..0181ae78b4 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -19,6 +19,8 @@ from erpnext.controllers.accounts_controller import update_child_qty_rate from erpnext.controllers.status_updater import OverAllowanceError from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order +from erpnext.stock.doctype.batch.test_batch import make_new_batch +from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials class TestPurchaseOrder(unittest.TestCase): def test_make_purchase_receipt(self): @@ -686,7 +688,7 @@ class TestPurchaseOrder(unittest.TestCase): def test_exploded_items_in_subcontracted(self): item_code = "_Test Subcontracted FG Item 1" - make_subcontracted_item(item_code) + make_subcontracted_item(item_code=item_code) po = create_purchase_order(item_code=item_code, qty=1, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", include_exploded_items=1) @@ -708,7 +710,7 @@ class TestPurchaseOrder(unittest.TestCase): def test_backflush_based_on_stock_entry(self): item_code = "_Test Subcontracted FG Item 1" - make_subcontracted_item(item_code) + make_subcontracted_item(item_code=item_code) make_item('Sub Contracted Raw Material 1', { 'is_stock_item': 1, 'is_sub_contracted_item': 1 @@ -767,6 +769,129 @@ class TestPurchaseOrder(unittest.TestCase): update_backflush_based_on("BOM") + def test_backflushed_based_on_for_multiple_batches(self): + item_code = "_Test Subcontracted FG Item 2" + make_item('Sub Contracted Raw Material 2', { + 'is_stock_item': 1, + 'is_sub_contracted_item': 1 + }) + + make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1, + raw_materials=["Sub Contracted Raw Material 2"]) + + update_backflush_based_on("Material Transferred for Subcontract") + + order_qty = 500 + po = create_purchase_order(item_code=item_code, qty=order_qty, + is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + + make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100) + + rm_items = [ + {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item", + "qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}] + + rm_item_string = json.dumps(rm_items) + se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) + se.submit() + + for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]: + make_new_batch(batch_id=batch, item_code=item_code) + + pr = make_purchase_receipt(po.name) + + # partial receipt + pr.get('items')[0].qty = 30 + pr.get('items')[0].batch_no = "ABCD1" + + purchase_order = po.name + purchase_order_item = po.items[0].name + + for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items(): + pr.append("items", { + "item_code": pr.get('items')[0].item_code, + "item_name": pr.get('items')[0].item_name, + "uom": pr.get('items')[0].uom, + "stock_uom": pr.get('items')[0].stock_uom, + "warehouse": pr.get('items')[0].warehouse, + "conversion_factor": pr.get('items')[0].conversion_factor, + "cost_center": pr.get('items')[0].cost_center, + "rate": pr.get('items')[0].rate, + "qty": qty, + "batch_no": batch_no, + "purchase_order": purchase_order, + "purchase_order_item": purchase_order_item + }) + + pr.submit() + + pr1 = make_purchase_receipt(po.name) + pr1.get('items')[0].qty = 300 + pr1.get('items')[0].batch_no = "ABCD1" + pr1.save() + + pr_key = ("Sub Contracted Raw Material 2", po.name) + consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key) + + self.assertTrue(pr1.supplied_items[0].consumed_qty > 0) + self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty)) + + update_backflush_based_on("BOM") + + def test_supplied_qty_against_subcontracted_po(self): + item_code = "_Test Subcontracted FG Item 5" + make_item('Sub Contracted Raw Material 4', { + 'is_stock_item': 1, + 'is_sub_contracted_item': 1 + }) + + make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"]) + + update_backflush_based_on("Material Transferred for Subcontract") + + order_qty = 250 + po = create_purchase_order(item_code=item_code, qty=order_qty, + is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", do_not_save=True) + + # Add same subcontracted items multiple times + po.append("items", { + "item_code": item_code, + "qty": order_qty, + "schedule_date": add_days(nowdate(), 1), + "warehouse": "_Test Warehouse - _TC" + }) + + po.set_missing_values() + po.submit() + + # Material receipt entry for the raw materials which will be send to supplier + make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Sub Contracted Raw Material 4", qty=500, basic_rate=100) + + rm_items = [ + { + "item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item", + "qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name + }, + { + "item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item", + "qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[1].name + }, + ] + + # Raw Materials transfer entry from stores to supplier's warehouse + rm_item_string = json.dumps(rm_items) + se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) + se.submit() + + po_doc = frappe.get_doc("Purchase Order", po.name) + for row in po_doc.supplied_items: + # Valid that whether transferred quantity is matching with supplied qty or not in the purchase order + self.assertEqual(row.supplied_qty, 250.0) + + update_backflush_based_on("BOM") + def test_advance_payment_entry_unlink_against_purchase_order(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry frappe.db.set_value("Accounts Settings", "Accounts Settings", @@ -839,27 +964,33 @@ def make_pr_against_po(po, received_qty=0): pr.submit() return pr -def make_subcontracted_item(item_code): +def make_subcontracted_item(**args): from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - if not frappe.db.exists('Item', item_code): - make_item(item_code, { + args = frappe._dict(args) + + if not frappe.db.exists('Item', args.item_code): + make_item(args.item_code, { 'is_stock_item': 1, - 'is_sub_contracted_item': 1 + 'is_sub_contracted_item': 1, + 'has_batch_no': args.get("has_batch_no") or 0 }) - if not frappe.db.exists('Item', "Test Extra Item 1"): - make_item("Test Extra Item 1", { - 'is_stock_item': 1, - }) + if not args.raw_materials: + if not frappe.db.exists('Item', "Test Extra Item 1"): + make_item("Test Extra Item 1", { + 'is_stock_item': 1, + }) - if not frappe.db.exists('Item', "Test Extra Item 2"): - make_item("Test Extra Item 2", { - 'is_stock_item': 1, - }) + if not frappe.db.exists('Item', "Test Extra Item 2"): + make_item("Test Extra Item 2", { + 'is_stock_item': 1, + }) - if not frappe.db.get_value('BOM', {'item': item_code}, 'name'): - make_bom(item = item_code, raw_materials = ['_Test FG Item', 'Test Extra Item 1']) + args.raw_materials = ['_Test FG Item', 'Test Extra Item 1'] + + if not frappe.db.get_value('BOM', {'item': args.item_code}, 'name'): + make_bom(item = args.item_code, raw_materials = args.get("raw_materials")) def update_backflush_based_on(based_on): doc = frappe.get_doc('Buying Settings') diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index e05c70e41c..f376836f7b 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _, msgprint from frappe.utils import flt,cint, cstr, getdate - +from six import iteritems from erpnext.accounts.party import get_party_details from erpnext.stock.get_item_details import get_conversion_factor from erpnext.buying.utils import validate_for_items, update_last_purchase_rate @@ -112,8 +112,8 @@ class BuyingController(StockController): "docstatus": 1 })] if self.is_return and len(not_cancelled_asset): - frappe.throw(_("{} has submitted assets linked to it. You need to cancel the assets to create purchase return.".format(self.return_against)), - title=_("Not Allowed")) + frappe.throw(_("{} has submitted assets linked to it. You need to cancel the assets to create purchase return.") + .format(self.return_against), title=_("Not Allowed")) def get_asset_items(self): if self.doctype not in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']: @@ -298,10 +298,10 @@ class BuyingController(StockController): title=_("Limit Crossed")) transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code) - backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) + # backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) for raw_material in transferred_raw_materials + non_stock_items: - rm_item_key = '{}{}'.format(raw_material.rm_item_code, item.purchase_order) + rm_item_key = (raw_material.rm_item_code, item.purchase_order) raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {}) consumed_qty = raw_material_data.get('qty', 0) @@ -330,8 +330,10 @@ class BuyingController(StockController): set_serial_nos(raw_material, consumed_serial_nos, qty) if raw_material.batch_nos: + backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {}) + batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code, - qty, transferred_batch_qty_map, backflushed_batch_qty_map) + qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order) for batch_data in batches_qty: qty = batch_data['qty'] raw_material.batch_no = batch_data['batch'] @@ -343,6 +345,10 @@ class BuyingController(StockController): rm = self.append('supplied_items', {}) rm.update(raw_material_data) + if not rm.main_item_code: + rm.main_item_code = fg_item_doc.item_code + + rm.reference_name = fg_item_doc.name rm.required_qty = qty rm.consumed_qty = qty @@ -792,8 +798,8 @@ class BuyingController(StockController): asset.set(field, None) asset.supplier = None if asset.docstatus == 1 and delete_asset: - frappe.throw(_('Cannot cancel this document as it is linked with submitted asset {0}.\ - Please cancel the it to continue.').format(frappe.utils.get_link_to_form('Asset', asset.name))) + frappe.throw(_('Cannot cancel this document as it is linked with submitted asset {0}. Please cancel it to continue.') + .format(frappe.utils.get_link_to_form('Asset', asset.name))) asset.flags.ignore_validate_update_after_submit = True asset.flags.ignore_mandatory = True @@ -873,7 +879,7 @@ def get_subcontracted_raw_materials_from_se(purchase_order, fg_item): AND se.purpose='Send to Subcontractor' AND se.purchase_order = %s AND IFNULL(sed.t_warehouse, '') != '' - AND sed.subcontracted_item = %s + AND IFNULL(sed.subcontracted_item, '') in ('', %s) GROUP BY sed.item_code, sed.subcontracted_item """ raw_materials = frappe.db.multisql({ @@ -890,39 +896,49 @@ def get_subcontracted_raw_materials_from_se(purchase_order, fg_item): return raw_materials def get_backflushed_subcontracted_raw_materials(purchase_orders): - common_query = """ - SELECT - CONCAT(prsi.rm_item_code, pri.purchase_order) AS item_key, - SUM(prsi.consumed_qty) AS qty, - {serial_no_concat_syntax} AS serial_nos, - {batch_no_concat_syntax} AS batch_nos - FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` prsi - WHERE - pr.name = pri.parent - AND pr.name = prsi.parent - AND pri.purchase_order IN %s - AND pri.item_code = prsi.main_item_code - AND pr.docstatus = 1 - GROUP BY prsi.rm_item_code, pri.purchase_order - """ + purchase_receipts = frappe.get_all("Purchase Receipt Item", + fields = ["purchase_order", "item_code", "name", "parent"], + filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))}) - backflushed_raw_materials = frappe.db.multisql({ - 'mariadb': common_query.format( - serial_no_concat_syntax="GROUP_CONCAT(prsi.serial_no)", - batch_no_concat_syntax="GROUP_CONCAT(prsi.batch_no)" - ), - 'postgres': common_query.format( - serial_no_concat_syntax="STRING_AGG(prsi.serial_no, ',')", - batch_no_concat_syntax="STRING_AGG(prsi.batch_no, ',')" - ) - }, (purchase_orders, ), as_dict=1) + distinct_purchase_receipts = {} + for pr in purchase_receipts: + key = (pr.purchase_order, pr.item_code, pr.parent) + distinct_purchase_receipts.setdefault(key, []).append(pr.name) backflushed_raw_materials_map = frappe._dict() - for item in backflushed_raw_materials: - backflushed_raw_materials_map.setdefault(item.item_key, item) + for args, references in iteritems(distinct_purchase_receipts): + purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references) + + for data in purchase_receipt_supplied_items: + pr_key = (data.rm_item_code, args[0]) + if pr_key not in backflushed_raw_materials_map: + backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({ + "qty": 0.0, + "serial_no": [], + "batch_no": [], + "consumed_batch": {} + })) + + row = backflushed_raw_materials_map.get(pr_key) + row.qty += data.consumed_qty + + for field in ["serial_no", "batch_no"]: + if data.get(field): + row[field].append(data.get(field)) + + if data.get("batch_no"): + if data.get("batch_no") in row.consumed_batch: + row.consumed_batch[data.get("batch_no")] += data.consumed_qty + else: + row.consumed_batch[data.get("batch_no")] = data.consumed_qty return backflushed_raw_materials_map +def get_supplied_items(item_code, purchase_receipt, references): + return frappe.get_all("Purchase Receipt Item Supplied", + fields=["rm_item_code", "consumed_qty", "serial_no", "batch_no"], + filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)}) + def get_asset_item_details(asset_items): asset_items_data = {} for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"], @@ -1004,14 +1020,15 @@ def get_transferred_batch_qty_map(purchase_order, fg_item): SELECT sed.batch_no, SUM(sed.qty) AS qty, - sed.item_code + sed.item_code, + sed.subcontracted_item FROM `tabStock Entry` se,`tabStock Entry Detail` sed WHERE se.name = sed.parent AND se.docstatus=1 AND se.purpose='Send to Subcontractor' AND se.purchase_order = %s - AND sed.subcontracted_item = %s + AND ifnull(sed.subcontracted_item, '') in ('', %s) AND sed.batch_no IS NOT NULL GROUP BY sed.batch_no, @@ -1019,8 +1036,10 @@ def get_transferred_batch_qty_map(purchase_order, fg_item): """, (purchase_order, fg_item), as_dict=1) for batch_data in transferred_batches: - transferred_batch_qty_map.setdefault((batch_data.item_code, fg_item), {}) - transferred_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty + key = ((batch_data.item_code, fg_item) + if batch_data.subcontracted_item else (batch_data.item_code, purchase_order)) + transferred_batch_qty_map.setdefault(key, {}) + transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty return transferred_batch_qty_map @@ -1057,10 +1076,11 @@ def get_backflushed_batch_qty_map(purchase_order, fg_item): return backflushed_batch_qty_map -def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batch_qty_map): +def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po): # Returns available batches to be backflushed based on requirements transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {}) - backflushed_batches = backflushed_batch_qty_map.get((item_code, fg_item), {}) + if not transferred_batches: + transferred_batches = transferred_batch_qty_map.get((item_code, po), {}) available_batches = [] diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 9e25ed0c99..a33d401b57 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -441,6 +441,7 @@ class TestSalesOrder(unittest.TestCase): def test_update_child_qty_rate_with_workflow(self): from frappe.model.workflow import apply_workflow + frappe.set_user("Administrator") workflow = make_sales_order_workflow() so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1) apply_workflow(so, 'Approve') diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 1fce5046f9..c2a3d3c151 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -256,3 +256,18 @@ class TestBatch(unittest.TestCase): batch.insert() return batch + +def make_new_batch(**args): + args = frappe._dict(args) + + try: + batch = frappe.get_doc({ + "doctype": "Batch", + "batch_id": args.batch_id, + "item": args.item_code, + }).insert() + + except frappe.DuplicateEntryError: + batch = frappe.get_doc("Batch", args.batch_id) + + return batch \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 12c89066a7..a1e01bf7ce 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -174,7 +174,7 @@ class TestPurchaseReceipt(unittest.TestCase): update_backflush_based_on("Material Transferred for Subcontract") item_code = "_Test Subcontracted FG Item 1" - make_subcontracted_item(item_code) + make_subcontracted_item(item_code=item_code) po = create_purchase_order(item_code=item_code, qty=1, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") @@ -717,6 +717,66 @@ class TestPurchaseReceipt(unittest.TestCase): # Allowed to submit for other company's PR self.assertEqual(pr.docstatus, 1) + def test_subcontracted_pr_for_multi_transfer_batches(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry, make_purchase_receipt + from erpnext.buying.doctype.purchase_order.test_purchase_order import (update_backflush_based_on, + create_purchase_order) + + update_backflush_based_on("Material Transferred for Subcontract") + item_code = "_Test Subcontracted FG Item 3" + + make_item('Sub Contracted Raw Material 3', { + 'is_stock_item': 1, + 'is_sub_contracted_item': 1, + 'has_batch_no': 1, + 'create_new_batch': 1 + }) + + create_subcontracted_item(item_code=item_code, has_batch_no=1, + raw_materials=["Sub Contracted Raw Material 3"]) + + order_qty = 500 + po = create_purchase_order(item_code=item_code, qty=order_qty, + is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + + ste1=make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Sub Contracted Raw Material 3", qty=300, basic_rate=100) + ste2=make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Sub Contracted Raw Material 3", qty=200, basic_rate=100) + + transferred_batch = { + ste1.items[0].batch_no : 300, + ste2.items[0].batch_no : 200 + } + + rm_items = [ + {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item", + "qty":300,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name}, + {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item", + "qty":200,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name} + ] + + rm_item_string = json.dumps(rm_items) + se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string)) + self.assertEqual(len(se.items), 2) + se.items[0].batch_no = ste1.items[0].batch_no + se.items[1].batch_no = ste2.items[0].batch_no + se.submit() + + supplied_qty = frappe.db.get_value("Purchase Order Item Supplied", + {"parent": po.name, "rm_item_code": "Sub Contracted Raw Material 3"}, "supplied_qty") + + self.assertEqual(supplied_qty, 500.00) + + pr = make_purchase_receipt(po.name) + pr.save() + self.assertEqual(len(pr.supplied_items), 2) + + for row in pr.supplied_items: + self.assertEqual(transferred_batch.get(row.batch_no), row.consumed_qty) + + update_backflush_based_on("BOM") def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference @@ -858,6 +918,33 @@ def make_purchase_receipt(**args): pr.submit() return pr +def create_subcontracted_item(**args): + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + args = frappe._dict(args) + + if not frappe.db.exists('Item', args.item_code): + make_item(args.item_code, { + 'is_stock_item': 1, + 'is_sub_contracted_item': 1, + 'has_batch_no': args.get("has_batch_no") or 0 + }) + + if not args.raw_materials: + if not frappe.db.exists('Item', "Test Extra Item 1"): + make_item("Test Extra Item 1", { + 'is_stock_item': 1, + }) + + if not frappe.db.exists('Item', "Test Extra Item 2"): + make_item("Test Extra Item 2", { + 'is_stock_item': 1, + }) + + args.raw_materials = ['_Test FG Item', 'Test Extra Item 1'] + + if not frappe.db.get_value('BOM', {'item': args.item_code}, 'name'): + make_bom(item = args.item_code, raw_materials = args.get("raw_materials")) test_dependencies = ["BOM", "Item Price", "Location"] test_records = frappe.get_test_records('Purchase Receipt') diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index dec4fe2da8..295149e238 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -343,10 +343,11 @@ def validate_material_transfer_entry(sle_doc): def validate_so_serial_no(sr, sales_order): if not sr.sales_order or sr.sales_order!= sales_order: - msg = _("Sales Order {0} has reservation for item {1}") - msg += _(", you can only deliver reserved {1} against {0}.") - msg += _(" Serial No {2} cannot be delivered") - frappe.throw(msg.format(sales_order, sr.item_code, sr.name)) + msg = (_("Sales Order {0} has reservation for the item {1}, you can only deliver reserved {1} against {0}.") + .format(sales_order, sr.item_code)) + + frappe.throw(_("""{0} Serial No {1} cannot be delivered""") + .format(msg, sr.name)) def has_duplicate_serial_no(sn, sle): if (sn.warehouse and not sle.skip_serial_no_validaiton @@ -449,6 +450,9 @@ def get_item_details(item_code): from tabItem where name=%s""", item_code, as_dict=True)[0] def get_serial_nos(serial_no): + if isinstance(serial_no, list): + return serial_no + return [s.strip() for s in cstr(serial_no).strip().upper().replace(',', '\n').split('\n') if s.strip()] @@ -572,8 +576,8 @@ def get_pos_reserved_serial_nos(filters): pos_transacted_sr_nos = frappe.db.sql("""select item.serial_no as serial_no from `tabPOS Invoice` p, `tabPOS Invoice Item` item - where p.name = item.parent - and p.consolidated_invoice is NULL + where p.name = item.parent + and p.consolidated_invoice is NULL and p.docstatus = 1 and item.docstatus = 1 and item.item_code = %(item_code)s @@ -605,7 +609,7 @@ def fetch_serial_numbers(filters, qty, do_not_include=[]): SELECT sr.name FROM `tabSerial No` sr {batch_join_selection} WHERE sr.name not in ({excluded_sr_nos}) AND - sr.item_code = %(item_code)s AND + sr.item_code = %(item_code)s AND sr.warehouse = %(warehouse)s AND ifnull(sr.sales_invoice,'') = '' AND ifnull(sr.delivery_document_no, '') = '' @@ -620,5 +624,5 @@ def fetch_serial_numbers(filters, qty, do_not_include=[]): batch_join_selection=batch_join_selection, batch_no_condition=batch_no_condition ), filters, as_dict=1) - + return serial_numbers \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 9ad3694c0a..4583c5174e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -571,8 +571,9 @@ class StockEntry(StockController): qty_allowance = flt(frappe.db.get_single_value("Buying Settings", "over_transfer_allowance")) - if (self.purpose == "Send to Subcontractor" and self.purchase_order and - backflush_raw_materials_based_on == 'BOM'): + if not (self.purpose == "Send to Subcontractor" and self.purchase_order): return + + if (backflush_raw_materials_based_on == 'BOM'): purchase_order = frappe.get_doc("Purchase Order", self.purchase_order) for se_item in self.items: item_code = se_item.original_item or se_item.item_code @@ -609,6 +610,11 @@ class StockEntry(StockController): if flt(total_supplied, precision) > flt(total_allowed, precision): frappe.throw(_("Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3}") .format(se_item.idx, se_item.item_code, total_allowed, self.purchase_order)) + elif backflush_raw_materials_based_on == "Material Transferred for Subcontract": + for row in self.items: + if not row.subcontracted_item: + frappe.throw(_("Row {0}: Subcontracted Item is mandatory for the raw material {1}") + .format(row.idx, frappe.bold(row.item_code))) def validate_bom(self): for d in self.get('items'): @@ -817,6 +823,13 @@ class StockEntry(StockController): ret.get('has_batch_no') and not args.get('batch_no')): args.batch_no = get_batch_no(args['item_code'], args['s_warehouse'], args['qty']) + if self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get('item_code'): + subcontract_items = frappe.get_all("Purchase Order Item Supplied", + {"parent": self.purchase_order, "rm_item_code": args.get('item_code')}, "main_item_code") + + if subcontract_items and len(subcontract_items) == 1: + ret["subcontracted_item"] = subcontract_items[0].main_item_code + return ret def set_items_for_stock_in(self): @@ -1288,9 +1301,16 @@ class StockEntry(StockController): #Update Supplied Qty in PO Supplied Items frappe.db.sql("""UPDATE `tabPurchase Order Item Supplied` pos - SET pos.supplied_qty = (SELECT ifnull(sum(transfer_qty), 0) FROM `tabStock Entry Detail` sed - WHERE pos.name = sed.po_detail and sed.docstatus = 1) - WHERE pos.docstatus = 1 and pos.parent = %s""", self.purchase_order) + SET + pos.supplied_qty = IFNULL((SELECT ifnull(sum(transfer_qty), 0) + FROM + `tabStock Entry Detail` sed, `tabStock Entry` se + WHERE + pos.name = sed.po_detail AND pos.rm_item_code = sed.item_code + AND pos.parent = se.purchase_order AND sed.docstatus = 1 + AND se.name = sed.parent and se.purchase_order = %(po)s + ), 0) + WHERE pos.docstatus = 1 and pos.parent = %(po)s""", {"po": self.purchase_order}) #Update reserved sub contracted quantity in bin based on Supplied Item Details and for d in self.get("items"): diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index ae2e3a134f..79e8f9af8f 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -1,5 +1,4 @@ { - "actions": [], "autoname": "hash", "creation": "2013-03-29 18:22:12", "doctype": "DocType", @@ -16,6 +15,7 @@ "item_code", "col_break2", "item_name", + "subcontracted_item", "section_break_8", "description", "column_break_10", @@ -57,7 +57,6 @@ "material_request", "material_request_item", "original_item", - "subcontracted_item", "reference_section", "against_stock_entry", "ste_detail", @@ -415,6 +414,7 @@ "read_only": 1 }, { + "depends_on": "eval:parent.purpose == 'Send to Subcontractor'", "fieldname": "subcontracted_item", "fieldtype": "Link", "label": "Subcontracted Item", @@ -504,7 +504,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-09-22 17:55:03.384138", + "modified": "2020-09-23 17:55:03.384138", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail",