mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-22 22:49:19 +00:00
feat: Validate sub assembly and material request items in Production Plan and fix Production Plan summary reports not showing correct received quantity from subcontracted POs
This commit is contained in:
@@ -469,6 +469,9 @@ class PurchaseOrder(BuyingController):
|
|||||||
if self.is_against_so():
|
if self.is_against_so():
|
||||||
self.update_status_updater()
|
self.update_status_updater()
|
||||||
|
|
||||||
|
if self.is_against_pp():
|
||||||
|
self.update_status_updater_if_from_pp()
|
||||||
|
|
||||||
self.update_prevdoc_status()
|
self.update_prevdoc_status()
|
||||||
if not self.is_subcontracted or self.is_old_subcontracting_flow:
|
if not self.is_subcontracted or self.is_old_subcontracting_flow:
|
||||||
self.update_requested_qty()
|
self.update_requested_qty()
|
||||||
@@ -550,6 +553,20 @@ class PurchaseOrder(BuyingController):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def update_status_updater_if_from_pp(self):
|
||||||
|
self.status_updater.append(
|
||||||
|
{
|
||||||
|
"source_dt": "Purchase Order Item",
|
||||||
|
"target_dt": "Production Plan Sub Assembly Item",
|
||||||
|
"join_field": "production_plan_sub_assembly_item",
|
||||||
|
"target_field": "ordered_qty",
|
||||||
|
"target_parent_dt": "Production Plan",
|
||||||
|
"target_parent_field": "",
|
||||||
|
"target_ref_field": "qty",
|
||||||
|
"source_field": "fg_item_qty",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def update_delivered_qty_in_sales_order(self):
|
def update_delivered_qty_in_sales_order(self):
|
||||||
"""Update delivered qty in Sales Order for drop ship"""
|
"""Update delivered qty in Sales Order for drop ship"""
|
||||||
sales_orders_to_update = []
|
sales_orders_to_update = []
|
||||||
@@ -570,6 +587,9 @@ class PurchaseOrder(BuyingController):
|
|||||||
def is_against_so(self):
|
def is_against_so(self):
|
||||||
return any(d.sales_order for d in self.items if d.sales_order)
|
return any(d.sales_order for d in self.items if d.sales_order)
|
||||||
|
|
||||||
|
def is_against_pp(self):
|
||||||
|
return any(d.production_plan for d in self.items if d.production_plan)
|
||||||
|
|
||||||
def set_received_qty_for_drop_ship_items(self):
|
def set_received_qty_for_drop_ship_items(self):
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if item.delivered_by_supplier == 1:
|
if item.delivered_by_supplier == 1:
|
||||||
|
|||||||
@@ -201,19 +201,19 @@ class StatusUpdater(Document):
|
|||||||
Get the status of the document.
|
Get the status of the document.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: A dictionary containing the status. This allows callers to receive
|
dict: A dictionary containing the status. This allows callers to receive
|
||||||
a dictionary for efficient bulk updates, for example when `per_billed`
|
a dictionary for efficient bulk updates, for example when `per_billed`
|
||||||
and other status fields also need to be updated.
|
and other status fields also need to be updated.
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
Can be overriden on a doctype to implement more localized status updater logic.
|
Can be overriden on a doctype to implement more localized status updater logic.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
{
|
{
|
||||||
"status": "Draft",
|
"status": "Draft",
|
||||||
"per_billed": 50,
|
"per_billed": 50,
|
||||||
"billing_status": "Partly Billed"
|
"billing_status": "Partly Billed"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
if self.doctype not in status_map:
|
if self.doctype not in status_map:
|
||||||
return {"status": self.status}
|
return {"status": self.status}
|
||||||
@@ -275,9 +275,20 @@ class StatusUpdater(Document):
|
|||||||
if d.doctype == args["source_dt"] and d.get(args["join_field"]):
|
if d.doctype == args["source_dt"] and d.get(args["join_field"]):
|
||||||
args["name"] = d.get(args["join_field"])
|
args["name"] = d.get(args["join_field"])
|
||||||
|
|
||||||
|
is_from_pp = (
|
||||||
|
hasattr(d, "production_plan_sub_assembly_item")
|
||||||
|
and frappe.db.get_value(
|
||||||
|
"Production Plan Sub Assembly Item",
|
||||||
|
d.production_plan_sub_assembly_item,
|
||||||
|
"type_of_manufacturing",
|
||||||
|
)
|
||||||
|
== "Subcontract"
|
||||||
|
)
|
||||||
|
args["item_code"] = "production_item" if is_from_pp else "item_code"
|
||||||
|
|
||||||
# get all qty where qty > target_field
|
# get all qty where qty > target_field
|
||||||
item = frappe.db.sql(
|
item = frappe.db.sql(
|
||||||
"""select item_code, `{target_ref_field}`,
|
"""select `{item_code}` as item_code, `{target_ref_field}`,
|
||||||
`{target_field}`, parenttype, parent from `tab{target_dt}`
|
`{target_field}`, parenttype, parent from `tab{target_dt}`
|
||||||
where `{target_ref_field}` < `{target_field}`
|
where `{target_ref_field}` < `{target_field}`
|
||||||
and name=%s and docstatus=1""".format(**args),
|
and name=%s and docstatus=1""".format(**args),
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Type",
|
"label": "Type",
|
||||||
"options": "\nPurchase\nMaterial Transfer\nMaterial Issue\nManufacture\nCustomer Provided"
|
"options": "\nPurchase\nMaterial Transfer\nMaterial Issue\nManufacture\nSubcontracting\nCustomer Provided"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_4",
|
"fieldname": "column_break_4",
|
||||||
@@ -115,9 +115,12 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"default": "0",
|
||||||
"fieldname": "requested_qty",
|
"fieldname": "requested_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Requested Qty",
|
"label": "Requested Qty",
|
||||||
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -202,7 +205,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:05.436575",
|
"modified": "2024-12-30 18:06:22.288340",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Material Request Plan Item",
|
"name": "Material Request Plan Item",
|
||||||
|
|||||||
@@ -21,7 +21,13 @@ class MaterialRequestPlanItem(Document):
|
|||||||
item_code: DF.Link
|
item_code: DF.Link
|
||||||
item_name: DF.Data | None
|
item_name: DF.Data | 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",
|
||||||
]
|
]
|
||||||
min_order_qty: DF.Float
|
min_order_qty: DF.Float
|
||||||
ordered_qty: DF.Float
|
ordered_qty: DF.Float
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, msgprint
|
from frappe import _, msgprint
|
||||||
@@ -722,6 +723,9 @@ class ProductionPlan(Document):
|
|||||||
if not wo_list:
|
if not wo_list:
|
||||||
frappe.msgprint(_("No Work Orders were created"))
|
frappe.msgprint(_("No Work Orders were created"))
|
||||||
|
|
||||||
|
if not po_list:
|
||||||
|
frappe.msgprint(_("No Purchase Orders were created"))
|
||||||
|
|
||||||
def make_work_order_for_finished_goods(self, wo_list, default_warehouses):
|
def make_work_order_for_finished_goods(self, wo_list, default_warehouses):
|
||||||
items_data = self.get_production_items()
|
items_data = self.get_production_items()
|
||||||
|
|
||||||
@@ -781,6 +785,34 @@ class ProductionPlan(Document):
|
|||||||
if not subcontracted_po:
|
if not subcontracted_po:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def calculate_sub_assembly_items():
|
||||||
|
items_to_remove = defaultdict(list)
|
||||||
|
for supplier, items in subcontracted_po.items():
|
||||||
|
for item in items:
|
||||||
|
table = frappe.qb.DocType("Purchase Order Item")
|
||||||
|
total_received_qty = (
|
||||||
|
frappe.qb.from_(table)
|
||||||
|
.select(
|
||||||
|
Sum(table.received_qty / (table.received_qty / table.fg_item_qty)).as_(
|
||||||
|
"total_received_qty"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(table.production_plan_sub_assembly_item == item.name) & (table.docstatus == 1)
|
||||||
|
)
|
||||||
|
).run(as_dict=True)[0].total_received_qty or 0
|
||||||
|
|
||||||
|
if item.qty == total_received_qty:
|
||||||
|
items_to_remove[supplier].append(item)
|
||||||
|
elif total_received_qty:
|
||||||
|
item.qty -= total_received_qty
|
||||||
|
|
||||||
|
subcontracted_po[supplier] = [item for item in items if item not in items_to_remove[supplier]]
|
||||||
|
|
||||||
|
return {key: value for key, value in subcontracted_po.items() if value}
|
||||||
|
|
||||||
|
subcontracted_po = calculate_sub_assembly_items()
|
||||||
|
|
||||||
for supplier, po_list in subcontracted_po.items():
|
for supplier, po_list in subcontracted_po.items():
|
||||||
po = frappe.new_doc("Purchase Order")
|
po = frappe.new_doc("Purchase Order")
|
||||||
po.company = self.company
|
po.company = self.company
|
||||||
@@ -847,13 +879,31 @@ class ProductionPlan(Document):
|
|||||||
except OverProductionError:
|
except OverProductionError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def validate_mr_subcontracted(self):
|
||||||
|
for row in self.mr_items:
|
||||||
|
if row.material_request_type == "Subcontracting":
|
||||||
|
if not frappe.db.get_value("Item", row.item_code, "is_sub_contracted_item"):
|
||||||
|
frappe.throw(
|
||||||
|
_("Item {0} is not a subcontracted item").format(row.item_code),
|
||||||
|
title=_("Invalid Item"),
|
||||||
|
)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_material_request(self):
|
def make_material_request(self):
|
||||||
|
self.validate_mr_subcontracted()
|
||||||
|
|
||||||
"""Create Material Requests grouped by Sales Order and Material Request Type"""
|
"""Create Material Requests grouped by Sales Order and Material Request Type"""
|
||||||
material_request_list = []
|
material_request_list = []
|
||||||
material_request_map = {}
|
material_request_map = {}
|
||||||
|
|
||||||
|
if all([item.requested_qty == item.quantity for item in self.mr_items]):
|
||||||
|
msgprint(_("All items are already requested"))
|
||||||
|
return
|
||||||
|
|
||||||
for item in self.mr_items:
|
for item in self.mr_items:
|
||||||
|
if item.quantity == item.requested_qty:
|
||||||
|
continue
|
||||||
|
|
||||||
item_doc = frappe.get_cached_doc("Item", item.item_code)
|
item_doc = frappe.get_cached_doc("Item", item.item_code)
|
||||||
|
|
||||||
material_request_type = item.material_request_type or item_doc.default_material_request_type
|
material_request_type = item.material_request_type or item_doc.default_material_request_type
|
||||||
@@ -887,7 +937,7 @@ class ProductionPlan(Document):
|
|||||||
"from_warehouse": item.from_warehouse
|
"from_warehouse": item.from_warehouse
|
||||||
if material_request_type == "Material Transfer"
|
if material_request_type == "Material Transfer"
|
||||||
else None,
|
else None,
|
||||||
"qty": item.quantity,
|
"qty": item.quantity - item.requested_qty,
|
||||||
"schedule_date": schedule_date,
|
"schedule_date": schedule_date,
|
||||||
"warehouse": item.warehouse,
|
"warehouse": item.warehouse,
|
||||||
"sales_order": item.sales_order,
|
"sales_order": item.sales_order,
|
||||||
@@ -1047,7 +1097,7 @@ class ProductionPlan(Document):
|
|||||||
filters={
|
filters={
|
||||||
"production_plan": self.name,
|
"production_plan": self.name,
|
||||||
"status": ("not in", ["Closed", "Stopped"]),
|
"status": ("not in", ["Closed", "Stopped"]),
|
||||||
"docstatus": ("<", 2),
|
"docstatus": 1,
|
||||||
},
|
},
|
||||||
fields="status",
|
fields="status",
|
||||||
pluck="status",
|
pluck="status",
|
||||||
|
|||||||
@@ -449,11 +449,38 @@ class TestProductionPlan(IntegrationTestCase):
|
|||||||
self.assertEqual(plan.sub_assembly_items[0].supplier, "_Test Supplier")
|
self.assertEqual(plan.sub_assembly_items[0].supplier, "_Test Supplier")
|
||||||
|
|
||||||
def test_production_plan_for_subcontracting_po(self):
|
def test_production_plan_for_subcontracting_po(self):
|
||||||
|
from erpnext.controllers.status_updater import OverAllowanceError
|
||||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||||
from erpnext.subcontracting.doctype.subcontracting_bom.test_subcontracting_bom import (
|
from erpnext.subcontracting.doctype.subcontracting_bom.test_subcontracting_bom import (
|
||||||
create_subcontracting_bom,
|
create_subcontracting_bom,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def make_purchase_receipt_from_po(po_doc):
|
||||||
|
from erpnext.buying.doctype.purchase_order.purchase_order import make_subcontracting_order
|
||||||
|
from erpnext.controllers.subcontracting_controller import make_rm_stock_entry
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
||||||
|
make_subcontracting_receipt,
|
||||||
|
)
|
||||||
|
from erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt import (
|
||||||
|
make_purchase_receipt as scr_make_purchase_receipt,
|
||||||
|
)
|
||||||
|
|
||||||
|
sco = make_subcontracting_order(po_doc.name)
|
||||||
|
sco.supplier_warehouse = "Work In Progress - _TC1"
|
||||||
|
sco.items[0].warehouse = "Finished Goods - _TC1"
|
||||||
|
sco.submit()
|
||||||
|
make_purchase_receipt(
|
||||||
|
qty=10,
|
||||||
|
item_code="Test Motherboard Wires 1",
|
||||||
|
company="_Test Company 1",
|
||||||
|
warehouse="Work In Progress - _TC1",
|
||||||
|
).submit()
|
||||||
|
make_rm_stock_entry(sco.name)
|
||||||
|
scr = make_subcontracting_receipt(sco.name)
|
||||||
|
scr.submit()
|
||||||
|
scr_make_purchase_receipt(scr.name).submit()
|
||||||
|
|
||||||
fg_item = "Test Motherboard 1"
|
fg_item = "Test Motherboard 1"
|
||||||
bom_tree_1 = {"Test Laptop 1": {fg_item: {"Test Motherboard Wires 1": {}}}}
|
bom_tree_1 = {"Test Laptop 1": {fg_item: {"Test Motherboard Wires 1": {}}}}
|
||||||
create_nested_bom(bom_tree_1, prefix="")
|
create_nested_bom(bom_tree_1, prefix="")
|
||||||
@@ -478,7 +505,12 @@ class TestProductionPlan(IntegrationTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
plan = create_production_plan(
|
plan = create_production_plan(
|
||||||
item_code="Test Laptop 1", planned_qty=10, use_multi_level_bom=1, do_not_submit=True
|
item_code="Test Laptop 1",
|
||||||
|
planned_qty=10,
|
||||||
|
use_multi_level_bom=1,
|
||||||
|
do_not_submit=True,
|
||||||
|
company="_Test Company 1",
|
||||||
|
skip_getting_mr_items=True,
|
||||||
)
|
)
|
||||||
plan.get_sub_assembly_items()
|
plan.get_sub_assembly_items()
|
||||||
plan.set_default_supplier_for_subcontracting_order()
|
plan.set_default_supplier_for_subcontracting_order()
|
||||||
@@ -492,10 +524,108 @@ class TestProductionPlan(IntegrationTestCase):
|
|||||||
self.assertEqual(po_doc.supplier, "_Test Supplier")
|
self.assertEqual(po_doc.supplier, "_Test Supplier")
|
||||||
self.assertEqual(po_doc.items[0].qty, 10.0)
|
self.assertEqual(po_doc.items[0].qty, 10.0)
|
||||||
self.assertEqual(po_doc.items[0].fg_item_qty, 10.0)
|
self.assertEqual(po_doc.items[0].fg_item_qty, 10.0)
|
||||||
self.assertEqual(po_doc.items[0].fg_item_qty, 10.0)
|
|
||||||
self.assertEqual(po_doc.items[0].fg_item, fg_item)
|
self.assertEqual(po_doc.items[0].fg_item, fg_item)
|
||||||
self.assertEqual(po_doc.items[0].item_code, service_item)
|
self.assertEqual(po_doc.items[0].item_code, service_item)
|
||||||
|
|
||||||
|
po_doc.items[0].qty = 11
|
||||||
|
po_doc.items[0].fg_item_qty = 11
|
||||||
|
|
||||||
|
# Test - 1 : Quantity of item cannot exceed quantity in production plan
|
||||||
|
self.assertRaises(OverAllowanceError, po_doc.submit)
|
||||||
|
|
||||||
|
po_doc.cancel()
|
||||||
|
po_doc = frappe.copy_doc(po_doc)
|
||||||
|
po_doc.items[0].qty = 5
|
||||||
|
po_doc.items[0].fg_item_qty = 5
|
||||||
|
po_doc.submit()
|
||||||
|
make_purchase_receipt_from_po(po_doc)
|
||||||
|
|
||||||
|
plan.make_work_order()
|
||||||
|
po = frappe.db.get_value("Purchase Order Item", {"production_plan": plan.name}, "parent")
|
||||||
|
po_doc = frappe.get_doc("Purchase Order", po)
|
||||||
|
|
||||||
|
# Test - 2 : Quantity of item in new PO should be the available quantity from Production Plan
|
||||||
|
self.assertEqual(po_doc.items[0].qty, 5.0)
|
||||||
|
|
||||||
|
po_doc.submit()
|
||||||
|
plan.make_work_order()
|
||||||
|
|
||||||
|
# Test - 3 : New POs should not be created since the quantity is already fulfilled
|
||||||
|
self.assertEqual(
|
||||||
|
frappe.db.count("Purchase Order Item", {"production_plan": plan.name, "docstatus": 1}), 2
|
||||||
|
) # 2 since we have already created and submitted 2 POs
|
||||||
|
|
||||||
|
def test_production_plan_for_mr_items(self):
|
||||||
|
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||||
|
|
||||||
|
def setup_item(fg_item):
|
||||||
|
item_doc = frappe.get_doc("Item", fg_item)
|
||||||
|
company = "_Test Company"
|
||||||
|
|
||||||
|
item_doc.is_sub_contracted_item = 1
|
||||||
|
for row in item_doc.item_defaults:
|
||||||
|
if row.company == company and not row.default_supplier:
|
||||||
|
row.default_supplier = "_Test Supplier"
|
||||||
|
|
||||||
|
if not item_doc.item_defaults:
|
||||||
|
item_doc.append("item_defaults", {"company": company, "default_supplier": "_Test Supplier"})
|
||||||
|
|
||||||
|
item_doc.save()
|
||||||
|
|
||||||
|
fg_item = "Test Motherboard 1"
|
||||||
|
fg_item_2 = "Test CPU 1"
|
||||||
|
bom_tree_1 = {
|
||||||
|
"Test Laptop 1": {fg_item: {"Test Motherboard Wires 1": {}}, fg_item_2: {"Test Pins 1": {}}}
|
||||||
|
}
|
||||||
|
create_nested_bom(bom_tree_1, prefix="")
|
||||||
|
|
||||||
|
setup_item(fg_item)
|
||||||
|
setup_item(fg_item_2)
|
||||||
|
|
||||||
|
plan = create_production_plan(
|
||||||
|
item_code="Test Laptop 1", planned_qty=10, use_multi_level_bom=1, do_not_submit=True
|
||||||
|
)
|
||||||
|
plan.get_sub_assembly_items()
|
||||||
|
plan.set_default_supplier_for_subcontracting_order()
|
||||||
|
plan.submit()
|
||||||
|
|
||||||
|
plan.make_material_request()
|
||||||
|
mr_item = frappe.db.get_value("Material Request Item", {"production_plan": plan.name}, "parent")
|
||||||
|
mr_doc = frappe.get_doc("Material Request", mr_item)
|
||||||
|
mr_doc.submit()
|
||||||
|
plan.reload()
|
||||||
|
plan.make_material_request()
|
||||||
|
|
||||||
|
# Test 1 : No more MRs should be created as quantity from Production Plan is fulfilled
|
||||||
|
self.assertEqual(frappe.db.count("Material Request Item", {"production_plan": plan.name}), 2)
|
||||||
|
|
||||||
|
mr_doc.cancel()
|
||||||
|
plan.reload()
|
||||||
|
|
||||||
|
# Test 2 : Requested quantity should be updated in Production Plan on cancellation of MR
|
||||||
|
self.assertEqual(plan.mr_items[0].requested_qty, 0)
|
||||||
|
|
||||||
|
plan.make_material_request()
|
||||||
|
mr_item = frappe.db.get_value("Material Request Item", {"production_plan": plan.name}, "parent")
|
||||||
|
mr_doc = frappe.get_doc("Material Request", mr_item)
|
||||||
|
mr_doc.items[0].qty = 5
|
||||||
|
mr_doc.submit()
|
||||||
|
plan.reload()
|
||||||
|
plan.make_material_request()
|
||||||
|
mr_item = frappe.db.get_value("Material Request Item", {"production_plan": plan.name}, "parent")
|
||||||
|
mr_doc = frappe.get_doc("Material Request", mr_item)
|
||||||
|
|
||||||
|
# Test 3 : Since Item 2 has been fully requested, it should not be included in the new MR by default
|
||||||
|
self.assertEqual(len(mr_doc.items), 1)
|
||||||
|
|
||||||
|
# Test 4 : Quantity in new MR should be the available quantity from Production Plan
|
||||||
|
self.assertEqual(mr_doc.items[0].qty, 5.0)
|
||||||
|
|
||||||
|
mr_doc.items[0].qty = 6
|
||||||
|
|
||||||
|
# Test 5 : Quantity of item cannot exceed available quantity from Production Plan
|
||||||
|
self.assertRaises(frappe.ValidationError, mr_doc.submit)
|
||||||
|
|
||||||
def test_production_plan_combine_subassembly(self):
|
def test_production_plan_combine_subassembly(self):
|
||||||
"""
|
"""
|
||||||
Test combining Sub assembly items belonging to the same BOM in Prod Plan.
|
Test combining Sub assembly items belonging to the same BOM in Prod Plan.
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"purchase_order",
|
"purchase_order",
|
||||||
"production_plan_item",
|
"production_plan_item",
|
||||||
"column_break_7",
|
"column_break_7",
|
||||||
|
"ordered_qty",
|
||||||
"received_qty",
|
"received_qty",
|
||||||
"indent",
|
"indent",
|
||||||
"section_break_19",
|
"section_break_19",
|
||||||
@@ -204,12 +205,20 @@
|
|||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Produced Qty",
|
"label": "Produced Qty",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "ordered_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Ordered Qty",
|
||||||
|
"non_negative": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:20.876695",
|
"modified": "2024-12-20 17:00:15.335880",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Production Plan Sub Assembly Item",
|
"name": "Production Plan Sub Assembly Item",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class ProductionPlanSubAssemblyItem(Document):
|
|||||||
fg_warehouse: DF.Link | None
|
fg_warehouse: DF.Link | None
|
||||||
indent: DF.Int
|
indent: DF.Int
|
||||||
item_name: DF.Data | None
|
item_name: DF.Data | None
|
||||||
|
ordered_qty: DF.Float
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parent_item_code: DF.Link | None
|
parent_item_code: DF.Link | None
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ def get_production_plan_item_details(filters, data, order_details):
|
|||||||
"production_plan_item": row.name,
|
"production_plan_item": row.name,
|
||||||
"bom_no": row.bom_no,
|
"bom_no": row.bom_no,
|
||||||
"production_item": row.item_code,
|
"production_item": row.item_code,
|
||||||
|
"docstatus": 1,
|
||||||
},
|
},
|
||||||
pluck="name",
|
pluck="name",
|
||||||
)
|
)
|
||||||
@@ -84,20 +85,24 @@ def get_production_plan_sub_assembly_item_details(filters, row, production_plan_
|
|||||||
subcontracted_item = item.type_of_manufacturing == "Subcontract"
|
subcontracted_item = item.type_of_manufacturing == "Subcontract"
|
||||||
|
|
||||||
if subcontracted_item:
|
if subcontracted_item:
|
||||||
docname = frappe.get_value(
|
docnames = frappe.get_all(
|
||||||
"Purchase Order Item",
|
"Purchase Order Item",
|
||||||
{"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)},
|
filters={"production_plan_sub_assembly_item": item.name, "docstatus": 1},
|
||||||
"parent",
|
fields=["parent"],
|
||||||
|
order_by="creation",
|
||||||
|
pluck="parent",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
docname = frappe.get_value(
|
docnames = frappe.get_all(
|
||||||
"Work Order",
|
"Work Order",
|
||||||
{"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)},
|
filters={"production_plan_sub_assembly_item": item.name, "docstatus": 1},
|
||||||
"name",
|
fields=["name"],
|
||||||
|
order_by="creation",
|
||||||
|
pluck="name",
|
||||||
)
|
)
|
||||||
|
|
||||||
data.append(
|
for docname in docnames:
|
||||||
{
|
data_to_append = {
|
||||||
"indent": 1 + item.indent,
|
"indent": 1 + item.indent,
|
||||||
"item_code": item.production_item,
|
"item_code": item.production_item,
|
||||||
"item_name": item.item_name,
|
"item_name": item.item_name,
|
||||||
@@ -111,7 +116,9 @@ def get_production_plan_sub_assembly_item_details(filters, row, production_plan_
|
|||||||
"pending_qty": flt(item.qty)
|
"pending_qty": flt(item.qty)
|
||||||
- flt(order_details.get((docname, item.production_item), {}).get("produced_qty", 0)),
|
- flt(order_details.get((docname, item.production_item), {}).get("produced_qty", 0)),
|
||||||
}
|
}
|
||||||
)
|
if data[-1] and data[-1]["indent"] == data_to_append["indent"]:
|
||||||
|
data_to_append["pending_qty"] = data[-1]["pending_qty"] - data_to_append["produced_qty"]
|
||||||
|
data.append(data_to_append)
|
||||||
|
|
||||||
|
|
||||||
def get_work_order_details(filters, order_details):
|
def get_work_order_details(filters, order_details):
|
||||||
@@ -127,9 +134,11 @@ def get_purchase_order_details(filters, order_details):
|
|||||||
for row in frappe.get_all(
|
for row in frappe.get_all(
|
||||||
"Purchase Order Item",
|
"Purchase Order Item",
|
||||||
filters={"production_plan": filters.get("production_plan")},
|
filters={"production_plan": filters.get("production_plan")},
|
||||||
fields=["parent", "received_qty as produced_qty", "item_code"],
|
fields=["parent", "received_qty as produced_qty", "item_code", "fg_item", "fg_item_qty"],
|
||||||
):
|
):
|
||||||
order_details.setdefault((row.parent, row.item_code), row)
|
if row.fg_item:
|
||||||
|
row.produced_qty /= row.produced_qty / row.fg_item_qty or 1
|
||||||
|
order_details.setdefault((row.parent, row.fg_item or row.item_code), row)
|
||||||
|
|
||||||
|
|
||||||
def get_column(filters):
|
def get_column(filters):
|
||||||
|
|||||||
@@ -159,6 +159,24 @@ class MaterialRequest(BuyingController):
|
|||||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||||
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
|
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
|
||||||
|
|
||||||
|
self.validate_pp_qty()
|
||||||
|
|
||||||
|
def validate_pp_qty(self):
|
||||||
|
for item in self.items:
|
||||||
|
if item.material_request_plan_item:
|
||||||
|
qty_from_pp = frappe.db.get_value(
|
||||||
|
"Material Request Plan Item",
|
||||||
|
item.material_request_plan_item,
|
||||||
|
["quantity", "requested_qty"],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
if item.qty > (qty_from_pp.quantity - qty_from_pp.requested_qty):
|
||||||
|
frappe.throw(
|
||||||
|
_("Quantity cannot be greater than {0} for Item {1}").format(
|
||||||
|
qty_from_pp.quantity - qty_from_pp.requested_qty, item.item_code
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def before_update_after_submit(self):
|
def before_update_after_submit(self):
|
||||||
self.validate_schedule_date()
|
self.validate_schedule_date()
|
||||||
|
|
||||||
@@ -233,7 +251,7 @@ class MaterialRequest(BuyingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.update_requested_qty_in_production_plan()
|
self.update_requested_qty_in_production_plan(cancel=True)
|
||||||
self.update_requested_qty()
|
self.update_requested_qty()
|
||||||
|
|
||||||
def get_mr_items_ordered_qty(self, mr_items):
|
def get_mr_items_ordered_qty(self, mr_items):
|
||||||
@@ -337,11 +355,14 @@ class MaterialRequest(BuyingController):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_requested_qty_in_production_plan(self):
|
def update_requested_qty_in_production_plan(self, cancel=False):
|
||||||
production_plans = []
|
production_plans = []
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
if d.production_plan and d.material_request_plan_item:
|
if d.production_plan and d.material_request_plan_item:
|
||||||
qty = d.qty if self.docstatus == 1 else 0
|
requested_qty = frappe.get_value(
|
||||||
|
"Material Request Plan Item", d.material_request_plan_item, "requested_qty"
|
||||||
|
)
|
||||||
|
qty = (requested_qty + d.qty) if not cancel else (requested_qty - d.qty)
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Material Request Plan Item", d.material_request_plan_item, "requested_qty", qty
|
"Material Request Plan Item", d.material_request_plan_item, "requested_qty", qty
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user