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:
Mihir Kandoi
2024-12-23 13:11:44 +05:30
committed by GitHub
parent cf57cb73f0
commit b7699012b2
5 changed files with 119 additions and 13 deletions

View File

@@ -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

View File

@@ -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")
);
} }
} }

View File

@@ -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",

View File

@@ -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,
}, },

View File

@@ -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()