Merge pull request #23662 from rohitwaghchaure/backport-subcontracting-issues

fix: multiple subcontracting issues
This commit is contained in:
rohitwaghchaure 2020-10-26 14:10:50 +05:30 committed by GitHub
commit 8c961938ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 353 additions and 75 deletions

View File

@ -19,6 +19,8 @@ from erpnext.controllers.accounts_controller import update_child_qty_rate
from erpnext.controllers.status_updater import OverAllowanceError from erpnext.controllers.status_updater import OverAllowanceError
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order 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): class TestPurchaseOrder(unittest.TestCase):
def test_make_purchase_receipt(self): def test_make_purchase_receipt(self):
@ -686,7 +688,7 @@ class TestPurchaseOrder(unittest.TestCase):
def test_exploded_items_in_subcontracted(self): def test_exploded_items_in_subcontracted(self):
item_code = "_Test Subcontracted FG Item 1" 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, po = create_purchase_order(item_code=item_code, qty=1,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", include_exploded_items=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): def test_backflush_based_on_stock_entry(self):
item_code = "_Test Subcontracted FG Item 1" 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', { make_item('Sub Contracted Raw Material 1', {
'is_stock_item': 1, 'is_stock_item': 1,
'is_sub_contracted_item': 1 'is_sub_contracted_item': 1
@ -767,6 +769,129 @@ class TestPurchaseOrder(unittest.TestCase):
update_backflush_based_on("BOM") 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): def test_advance_payment_entry_unlink_against_purchase_order(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
frappe.db.set_value("Accounts Settings", "Accounts Settings", frappe.db.set_value("Accounts Settings", "Accounts Settings",
@ -839,15 +964,19 @@ def make_pr_against_po(po, received_qty=0):
pr.submit() pr.submit()
return pr 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 from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
if not frappe.db.exists('Item', item_code): args = frappe._dict(args)
make_item(item_code, {
if not frappe.db.exists('Item', args.item_code):
make_item(args.item_code, {
'is_stock_item': 1, '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 args.raw_materials:
if not frappe.db.exists('Item', "Test Extra Item 1"): if not frappe.db.exists('Item', "Test Extra Item 1"):
make_item("Test Extra Item 1", { make_item("Test Extra Item 1", {
'is_stock_item': 1, 'is_stock_item': 1,
@ -858,8 +987,10 @@ def make_subcontracted_item(item_code):
'is_stock_item': 1, 'is_stock_item': 1,
}) })
if not frappe.db.get_value('BOM', {'item': item_code}, 'name'): args.raw_materials = ['_Test FG Item', 'Test Extra Item 1']
make_bom(item = item_code, 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): def update_backflush_based_on(based_on):
doc = frappe.get_doc('Buying Settings') doc = frappe.get_doc('Buying Settings')

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _, msgprint from frappe import _, msgprint
from frappe.utils import flt,cint, cstr, getdate from frappe.utils import flt,cint, cstr, getdate
from six import iteritems
from erpnext.accounts.party import get_party_details from erpnext.accounts.party import get_party_details
from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
@ -112,8 +112,8 @@ class BuyingController(StockController):
"docstatus": 1 "docstatus": 1
})] })]
if self.is_return and len(not_cancelled_asset): 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)), frappe.throw(_("{} has submitted assets linked to it. You need to cancel the assets to create purchase return.")
title=_("Not Allowed")) .format(self.return_against), title=_("Not Allowed"))
def get_asset_items(self): def get_asset_items(self):
if self.doctype not in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']: if self.doctype not in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']:
@ -298,10 +298,10 @@ class BuyingController(StockController):
title=_("Limit Crossed")) title=_("Limit Crossed"))
transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code) 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: 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, {}) raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
consumed_qty = raw_material_data.get('qty', 0) consumed_qty = raw_material_data.get('qty', 0)
@ -330,8 +330,10 @@ class BuyingController(StockController):
set_serial_nos(raw_material, consumed_serial_nos, qty) set_serial_nos(raw_material, consumed_serial_nos, qty)
if raw_material.batch_nos: 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, 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: for batch_data in batches_qty:
qty = batch_data['qty'] qty = batch_data['qty']
raw_material.batch_no = batch_data['batch'] raw_material.batch_no = batch_data['batch']
@ -343,6 +345,10 @@ class BuyingController(StockController):
rm = self.append('supplied_items', {}) rm = self.append('supplied_items', {})
rm.update(raw_material_data) 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.required_qty = qty
rm.consumed_qty = qty rm.consumed_qty = qty
@ -792,8 +798,8 @@ class BuyingController(StockController):
asset.set(field, None) asset.set(field, None)
asset.supplier = None asset.supplier = None
if asset.docstatus == 1 and delete_asset: if asset.docstatus == 1 and delete_asset:
frappe.throw(_('Cannot cancel this document as it is linked with submitted asset {0}.\ frappe.throw(_('Cannot cancel this document as it is linked with submitted asset {0}. Please cancel it to continue.')
Please cancel the it to continue.').format(frappe.utils.get_link_to_form('Asset', asset.name))) .format(frappe.utils.get_link_to_form('Asset', asset.name)))
asset.flags.ignore_validate_update_after_submit = True asset.flags.ignore_validate_update_after_submit = True
asset.flags.ignore_mandatory = 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.purpose='Send to Subcontractor'
AND se.purchase_order = %s AND se.purchase_order = %s
AND IFNULL(sed.t_warehouse, '') != '' 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 GROUP BY sed.item_code, sed.subcontracted_item
""" """
raw_materials = frappe.db.multisql({ raw_materials = frappe.db.multisql({
@ -890,39 +896,49 @@ def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
return raw_materials return raw_materials
def get_backflushed_subcontracted_raw_materials(purchase_orders): def get_backflushed_subcontracted_raw_materials(purchase_orders):
common_query = """ purchase_receipts = frappe.get_all("Purchase Receipt Item",
SELECT fields = ["purchase_order", "item_code", "name", "parent"],
CONCAT(prsi.rm_item_code, pri.purchase_order) AS item_key, filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))})
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
"""
backflushed_raw_materials = frappe.db.multisql({ distinct_purchase_receipts = {}
'mariadb': common_query.format( for pr in purchase_receipts:
serial_no_concat_syntax="GROUP_CONCAT(prsi.serial_no)", key = (pr.purchase_order, pr.item_code, pr.parent)
batch_no_concat_syntax="GROUP_CONCAT(prsi.batch_no)" distinct_purchase_receipts.setdefault(key, []).append(pr.name)
),
'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)
backflushed_raw_materials_map = frappe._dict() backflushed_raw_materials_map = frappe._dict()
for item in backflushed_raw_materials: for args, references in iteritems(distinct_purchase_receipts):
backflushed_raw_materials_map.setdefault(item.item_key, item) 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 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): def get_asset_item_details(asset_items):
asset_items_data = {} asset_items_data = {}
for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"], 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 SELECT
sed.batch_no, sed.batch_no,
SUM(sed.qty) AS qty, SUM(sed.qty) AS qty,
sed.item_code sed.item_code,
sed.subcontracted_item
FROM `tabStock Entry` se,`tabStock Entry Detail` sed FROM `tabStock Entry` se,`tabStock Entry Detail` sed
WHERE WHERE
se.name = sed.parent se.name = sed.parent
AND se.docstatus=1 AND se.docstatus=1
AND se.purpose='Send to Subcontractor' AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s AND se.purchase_order = %s
AND sed.subcontracted_item = %s AND ifnull(sed.subcontracted_item, '') in ('', %s)
AND sed.batch_no IS NOT NULL AND sed.batch_no IS NOT NULL
GROUP BY GROUP BY
sed.batch_no, sed.batch_no,
@ -1019,8 +1036,10 @@ def get_transferred_batch_qty_map(purchase_order, fg_item):
""", (purchase_order, fg_item), as_dict=1) """, (purchase_order, fg_item), as_dict=1)
for batch_data in transferred_batches: for batch_data in transferred_batches:
transferred_batch_qty_map.setdefault((batch_data.item_code, fg_item), {}) key = ((batch_data.item_code, fg_item)
transferred_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty 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 return transferred_batch_qty_map
@ -1057,10 +1076,11 @@ def get_backflushed_batch_qty_map(purchase_order, fg_item):
return backflushed_batch_qty_map 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 # Returns available batches to be backflushed based on requirements
transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {}) 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 = [] available_batches = []

View File

@ -441,6 +441,7 @@ class TestSalesOrder(unittest.TestCase):
def test_update_child_qty_rate_with_workflow(self): def test_update_child_qty_rate_with_workflow(self):
from frappe.model.workflow import apply_workflow from frappe.model.workflow import apply_workflow
frappe.set_user("Administrator")
workflow = make_sales_order_workflow() workflow = make_sales_order_workflow()
so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1) so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1)
apply_workflow(so, 'Approve') apply_workflow(so, 'Approve')

View File

@ -256,3 +256,18 @@ class TestBatch(unittest.TestCase):
batch.insert() batch.insert()
return batch 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

View File

@ -174,7 +174,7 @@ class TestPurchaseReceipt(unittest.TestCase):
update_backflush_based_on("Material Transferred for Subcontract") update_backflush_based_on("Material Transferred for Subcontract")
item_code = "_Test Subcontracted FG Item 1" 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, po = create_purchase_order(item_code=item_code, qty=1,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") 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 # Allowed to submit for other company's PR
self.assertEqual(pr.docstatus, 1) 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): def get_sl_entries(voucher_type, voucher_no):
return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
@ -858,6 +918,33 @@ def make_purchase_receipt(**args):
pr.submit() pr.submit()
return pr 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_dependencies = ["BOM", "Item Price", "Location"]
test_records = frappe.get_test_records('Purchase Receipt') test_records = frappe.get_test_records('Purchase Receipt')

View File

@ -343,10 +343,11 @@ def validate_material_transfer_entry(sle_doc):
def validate_so_serial_no(sr, sales_order): def validate_so_serial_no(sr, sales_order):
if not sr.sales_order or sr.sales_order!= sales_order: if not sr.sales_order or sr.sales_order!= sales_order:
msg = _("Sales Order {0} has reservation for item {1}") msg = (_("Sales Order {0} has reservation for the item {1}, you can only deliver reserved {1} against {0}.")
msg += _(", you can only deliver reserved {1} against {0}.") .format(sales_order, sr.item_code))
msg += _(" Serial No {2} cannot be delivered")
frappe.throw(msg.format(sales_order, sr.item_code, sr.name)) frappe.throw(_("""{0} Serial No {1} cannot be delivered""")
.format(msg, sr.name))
def has_duplicate_serial_no(sn, sle): def has_duplicate_serial_no(sn, sle):
if (sn.warehouse and not sle.skip_serial_no_validaiton 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] from tabItem where name=%s""", item_code, as_dict=True)[0]
def get_serial_nos(serial_no): 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') return [s.strip() for s in cstr(serial_no).strip().upper().replace(',', '\n').split('\n')
if s.strip()] if s.strip()]

View File

@ -571,8 +571,9 @@ class StockEntry(StockController):
qty_allowance = flt(frappe.db.get_single_value("Buying Settings", qty_allowance = flt(frappe.db.get_single_value("Buying Settings",
"over_transfer_allowance")) "over_transfer_allowance"))
if (self.purpose == "Send to Subcontractor" and self.purchase_order and if not (self.purpose == "Send to Subcontractor" and self.purchase_order): return
backflush_raw_materials_based_on == 'BOM'):
if (backflush_raw_materials_based_on == 'BOM'):
purchase_order = frappe.get_doc("Purchase Order", self.purchase_order) purchase_order = frappe.get_doc("Purchase Order", self.purchase_order)
for se_item in self.items: for se_item in self.items:
item_code = se_item.original_item or se_item.item_code 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): 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}") 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)) .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): def validate_bom(self):
for d in self.get('items'): for d in self.get('items'):
@ -817,6 +823,13 @@ class StockEntry(StockController):
ret.get('has_batch_no') and not args.get('batch_no')): 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']) 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 return ret
def set_items_for_stock_in(self): def set_items_for_stock_in(self):
@ -1288,9 +1301,16 @@ class StockEntry(StockController):
#Update Supplied Qty in PO Supplied Items #Update Supplied Qty in PO Supplied Items
frappe.db.sql("""UPDATE `tabPurchase Order Item Supplied` pos frappe.db.sql("""UPDATE `tabPurchase Order Item Supplied` pos
SET pos.supplied_qty = (SELECT ifnull(sum(transfer_qty), 0) FROM `tabStock Entry Detail` sed SET
WHERE pos.name = sed.po_detail and sed.docstatus = 1) pos.supplied_qty = IFNULL((SELECT ifnull(sum(transfer_qty), 0)
WHERE pos.docstatus = 1 and pos.parent = %s""", self.purchase_order) 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 #Update reserved sub contracted quantity in bin based on Supplied Item Details and
for d in self.get("items"): for d in self.get("items"):

View File

@ -1,5 +1,4 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-03-29 18:22:12", "creation": "2013-03-29 18:22:12",
"doctype": "DocType", "doctype": "DocType",
@ -16,6 +15,7 @@
"item_code", "item_code",
"col_break2", "col_break2",
"item_name", "item_name",
"subcontracted_item",
"section_break_8", "section_break_8",
"description", "description",
"column_break_10", "column_break_10",
@ -57,7 +57,6 @@
"material_request", "material_request",
"material_request_item", "material_request_item",
"original_item", "original_item",
"subcontracted_item",
"reference_section", "reference_section",
"against_stock_entry", "against_stock_entry",
"ste_detail", "ste_detail",
@ -415,6 +414,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:parent.purpose == 'Send to Subcontractor'",
"fieldname": "subcontracted_item", "fieldname": "subcontracted_item",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Subcontracted Item", "label": "Subcontracted Item",
@ -504,7 +504,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-09-22 17:55:03.384138", "modified": "2020-09-23 17:55:03.384138",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry Detail", "name": "Stock Entry Detail",