feat: Create subcontracted PO from Material Request (#44745)
* feat: Create subcontracted PO from Material Request * fix: Made minor changes in logic to pass all test cases * refactor: Made changes suggested by mentor and simplified logic * test: Made changes to tests
This commit is contained in:
@@ -421,6 +421,13 @@ class StatusUpdater(Document):
|
|||||||
if d.doctype != args["source_dt"]:
|
if d.doctype != args["source_dt"]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
d.get("material_request")
|
||||||
|
and frappe.db.get_value("Material Request", d.material_request, "material_request_type")
|
||||||
|
== "Subcontracting"
|
||||||
|
):
|
||||||
|
args.update({"source_field": "fg_item_qty"})
|
||||||
|
|
||||||
self._update_modified(args, update_modified)
|
self._update_modified(args, update_modified)
|
||||||
|
|
||||||
# updates qty in the child table
|
# updates qty in the child table
|
||||||
|
|||||||
@@ -122,6 +122,12 @@ frappe.ui.form.on("Material Request", {
|
|||||||
() => frm.events.make_purchase_order(frm),
|
() => frm.events.make_purchase_order(frm),
|
||||||
__("Create")
|
__("Create")
|
||||||
);
|
);
|
||||||
|
} else if (frm.doc.material_request_type === "Subcontracting") {
|
||||||
|
frm.add_custom_button(
|
||||||
|
__("Subcontracted Purchase Order"),
|
||||||
|
() => frm.events.make_purchase_order(frm),
|
||||||
|
__("Create")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Purpose",
|
"label": "Purpose",
|
||||||
"options": "Purchase\nMaterial Transfer\nMaterial Issue\nManufacture\nCustomer Provided",
|
"options": "Purchase\nMaterial Transfer\nMaterial Issue\nManufacture\nSubcontracting\nCustomer Provided",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -357,7 +357,7 @@
|
|||||||
"idx": 70,
|
"idx": 70,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:04.971211",
|
"modified": "2024-12-16 12:46:02.262167",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Material Request",
|
"name": "Material Request",
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ from erpnext.controllers.buying_controller import BuyingController
|
|||||||
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
||||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||||
from erpnext.stock.stock_balance import get_indented_qty, update_bin_qty
|
from erpnext.stock.stock_balance import get_indented_qty, update_bin_qty
|
||||||
|
from erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom import (
|
||||||
|
get_subcontracting_boms_for_finished_goods,
|
||||||
|
)
|
||||||
|
|
||||||
form_grid_templates = {"items": "templates/form_grid/material_request_grid.html"}
|
form_grid_templates = {"items": "templates/form_grid/material_request_grid.html"}
|
||||||
|
|
||||||
@@ -40,7 +43,12 @@ class MaterialRequest(BuyingController):
|
|||||||
job_card: DF.Link | None
|
job_card: DF.Link | None
|
||||||
letter_head: DF.Link | None
|
letter_head: DF.Link | None
|
||||||
material_request_type: DF.Literal[
|
material_request_type: DF.Literal[
|
||||||
"Purchase", "Material Transfer", "Material Issue", "Manufacture", "Customer Provided"
|
"Purchase",
|
||||||
|
"Material Transfer",
|
||||||
|
"Material Issue",
|
||||||
|
"Manufacture",
|
||||||
|
"Subcontracting",
|
||||||
|
"Customer Provided",
|
||||||
]
|
]
|
||||||
naming_series: DF.Literal["MAT-MR-.YYYY.-"]
|
naming_series: DF.Literal["MAT-MR-.YYYY.-"]
|
||||||
per_ordered: DF.Percent
|
per_ordered: DF.Percent
|
||||||
@@ -385,6 +393,22 @@ def update_item(obj, target, source_parent):
|
|||||||
if getdate(target.schedule_date) < getdate(nowdate()):
|
if getdate(target.schedule_date) < getdate(nowdate()):
|
||||||
target.schedule_date = None
|
target.schedule_date = None
|
||||||
|
|
||||||
|
if target.fg_item:
|
||||||
|
target.fg_item_qty = obj.stock_qty
|
||||||
|
if sc_bom := get_subcontracting_boms_for_finished_goods(target.fg_item):
|
||||||
|
target.item_code = sc_bom.service_item
|
||||||
|
target.uom = sc_bom.service_item_uom
|
||||||
|
target.conversion_factor = (
|
||||||
|
frappe.db.get_value(
|
||||||
|
"UOM Conversion Detail",
|
||||||
|
{"parent": sc_bom.service_item, "uom": sc_bom.service_item_uom},
|
||||||
|
"conversion_factor",
|
||||||
|
)
|
||||||
|
or 1
|
||||||
|
)
|
||||||
|
target.qty = target.fg_item_qty * sc_bom.conversion_factor
|
||||||
|
target.stock_qty = target.qty * target.conversion_factor
|
||||||
|
|
||||||
|
|
||||||
def get_list_context(context=None):
|
def get_list_context(context=None):
|
||||||
from erpnext.controllers.website_list_for_contact import get_list_context
|
from erpnext.controllers.website_list_for_contact import get_list_context
|
||||||
@@ -416,11 +440,18 @@ def make_purchase_order(source_name, target_doc=None, args=None):
|
|||||||
if isinstance(args, str):
|
if isinstance(args, str):
|
||||||
args = json.loads(args)
|
args = json.loads(args)
|
||||||
|
|
||||||
|
is_subcontracted = (
|
||||||
|
frappe.db.get_value("Material Request", source_name, "material_request_type") == "Subcontracting"
|
||||||
|
)
|
||||||
|
|
||||||
def postprocess(source, target_doc):
|
def postprocess(source, target_doc):
|
||||||
|
target_doc.is_subcontracted = is_subcontracted
|
||||||
if frappe.flags.args and frappe.flags.args.default_supplier:
|
if frappe.flags.args and frappe.flags.args.default_supplier:
|
||||||
# items only for given default supplier
|
# items only for given default supplier
|
||||||
supplier_items = []
|
supplier_items = []
|
||||||
for d in target_doc.items:
|
for d in target_doc.items:
|
||||||
|
if is_subcontracted and not d.item_code:
|
||||||
|
continue
|
||||||
default_supplier = get_item_defaults(d.item_code, target_doc.company).get("default_supplier")
|
default_supplier = get_item_defaults(d.item_code, target_doc.company).get("default_supplier")
|
||||||
if frappe.flags.args.default_supplier == default_supplier:
|
if frappe.flags.args.default_supplier == default_supplier:
|
||||||
supplier_items.append(d)
|
supplier_items.append(d)
|
||||||
@@ -436,25 +467,37 @@ def make_purchase_order(source_name, target_doc=None, args=None):
|
|||||||
|
|
||||||
return qty < d.stock_qty and child_filter
|
return qty < d.stock_qty and child_filter
|
||||||
|
|
||||||
|
def generate_field_map():
|
||||||
|
field_map = [
|
||||||
|
["name", "material_request_item"],
|
||||||
|
["parent", "material_request"],
|
||||||
|
["sales_order", "sales_order"],
|
||||||
|
["sales_order_item", "sales_order_item"],
|
||||||
|
["wip_composite_asset", "wip_composite_asset"],
|
||||||
|
]
|
||||||
|
|
||||||
|
if is_subcontracted:
|
||||||
|
field_map.extend([["item_code", "fg_item"], ["qty", "fg_item_qty"]])
|
||||||
|
else:
|
||||||
|
field_map.extend([["uom", "stock_uom"], ["uom", "uom"]])
|
||||||
|
|
||||||
|
return field_map
|
||||||
|
|
||||||
doclist = get_mapped_doc(
|
doclist = get_mapped_doc(
|
||||||
"Material Request",
|
"Material Request",
|
||||||
source_name,
|
source_name,
|
||||||
{
|
{
|
||||||
"Material Request": {
|
"Material Request": {
|
||||||
"doctype": "Purchase Order",
|
"doctype": "Purchase Order",
|
||||||
"validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]},
|
"validation": {
|
||||||
|
"docstatus": ["=", 1],
|
||||||
|
"material_request_type": ["in", ["Purchase", "Subcontracting"]],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"Material Request Item": {
|
"Material Request Item": {
|
||||||
"doctype": "Purchase Order Item",
|
"doctype": "Purchase Order Item",
|
||||||
"field_map": [
|
"field_map": generate_field_map(),
|
||||||
["name", "material_request_item"],
|
"field_no_map": ["item_code", "item_name", "qty"] if is_subcontracted else [],
|
||||||
["parent", "material_request"],
|
|
||||||
["uom", "stock_uom"],
|
|
||||||
["uom", "uom"],
|
|
||||||
["sales_order", "sales_order"],
|
|
||||||
["sales_order_item", "sales_order_item"],
|
|
||||||
["wip_composite_asset", "wip_composite_asset"],
|
|
||||||
],
|
|
||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": select_item,
|
"condition": select_item,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
import frappe.model
|
||||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||||
from frappe.utils import flt, today
|
from frappe.utils import flt, today
|
||||||
|
|
||||||
@@ -53,6 +54,55 @@ class TestMaterialRequest(IntegrationTestCase):
|
|||||||
self.assertEqual(po.doctype, "Purchase Order")
|
self.assertEqual(po.doctype, "Purchase Order")
|
||||||
self.assertEqual(len(po.get("items")), len(mr.get("items")))
|
self.assertEqual(len(po.get("items")), len(mr.get("items")))
|
||||||
|
|
||||||
|
def test_make_subcontracted_purchase_order(self):
|
||||||
|
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||||
|
from erpnext.stock.doctype.item.test_item import create_item, make_item
|
||||||
|
from erpnext.subcontracting.doctype.subcontracting_bom.test_subcontracting_bom import (
|
||||||
|
create_subcontracting_bom,
|
||||||
|
)
|
||||||
|
|
||||||
|
mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0]).insert()
|
||||||
|
mr.material_request_type = "Subcontracting"
|
||||||
|
mr.submit()
|
||||||
|
|
||||||
|
frappe.db.set_value("Item", mr.items[0].item_code, "is_sub_contracted_item", 1)
|
||||||
|
|
||||||
|
raw_materials = ["Raw Material Item 1", "Raw Material Item 2"]
|
||||||
|
for item in raw_materials:
|
||||||
|
create_item(item)
|
||||||
|
|
||||||
|
frappe.new_doc("UOM").update({"uom_name": "Test UOM"}).save()
|
||||||
|
service_item = make_item(
|
||||||
|
properties={"is_stock_item": 0}, uoms=[{"uom": "Test UOM", "conversion_factor": 3}]
|
||||||
|
)
|
||||||
|
|
||||||
|
mr.items[0].default_bom = make_bom(item=mr.items[0].item_code, raw_materials=raw_materials)
|
||||||
|
mr.reload()
|
||||||
|
|
||||||
|
create_subcontracting_bom(
|
||||||
|
finished_good=mr.items[0].item_code,
|
||||||
|
service_item=service_item.name,
|
||||||
|
finished_good_qty=2,
|
||||||
|
service_item_qty=1,
|
||||||
|
service_item_uom="Test UOM",
|
||||||
|
)
|
||||||
|
|
||||||
|
po = make_purchase_order(mr.name)
|
||||||
|
po.supplier = "_Test Supplier"
|
||||||
|
po.items[0].schedule_date = today()
|
||||||
|
po.items.pop(1)
|
||||||
|
|
||||||
|
# Test 1 - Test if items stock qty, qty and finished good qty are calculated correctly based on provided UOMs
|
||||||
|
self.assertEqual(po.items[0].stock_qty, 81)
|
||||||
|
self.assertEqual(po.items[0].qty, 27)
|
||||||
|
self.assertEqual(po.items[0].fg_item_qty, 54)
|
||||||
|
|
||||||
|
po.submit()
|
||||||
|
mr.reload()
|
||||||
|
|
||||||
|
# Test 2 - MR items ordered qty should be updated based on PO items qty when submitted
|
||||||
|
self.assertEqual(mr.items[0].ordered_qty, 54)
|
||||||
|
|
||||||
def test_make_supplier_quotation(self):
|
def test_make_supplier_quotation(self):
|
||||||
mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0]).insert()
|
mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0]).insert()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user