feat: demand planning, MPS and MRP

This commit is contained in:
Rohit Waghchaure
2025-08-08 21:15:42 +05:30
parent 993ba4cf45
commit f7a37d2812
49 changed files with 4356 additions and 17 deletions

View File

@@ -16,6 +16,7 @@
"order_confirmation_no",
"order_confirmation_date",
"get_items_from_open_material_requests",
"mps",
"column_break_7",
"transaction_date",
"schedule_date",
@@ -1315,6 +1316,13 @@
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
},
{
"fieldname": "mps",
"fieldtype": "Link",
"label": "MPS",
"options": "Master Production Schedule",
"read_only": 1
}
],
"grid_page_length": 50,
@@ -1322,7 +1330,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2025-07-31 17:19:40.816883",
"modified": "2025-08-28 11:00:56.635116",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@@ -108,6 +108,7 @@ class PurchaseOrder(BuyingController):
items: DF.Table[PurchaseOrderItem]
language: DF.Data | None
letter_head: DF.Link | None
mps: DF.Link | None
named_place: DF.Data | None
naming_series: DF.Literal["PUR-ORD-.YYYY.-"]
net_total: DF.Currency

View File

@@ -0,0 +1,186 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Master Production Schedule", {
refresh(frm) {
frm.trigger("set_query_filters");
frm.set_df_property("items", "cannot_add_rows", true);
frm.fields_dict.items.$wrapper.find("[data-action='duplicate_rows']").css("display", "none");
frm.trigger("set_custom_buttons");
},
setup(frm) {
frm.trigger("set_indicator_for_item");
},
set_indicator_for_item(frm) {
frm.set_indicator_formatter("item_code", function (doc) {
if (doc.order_release_date < frappe.datetime.get_today()) {
return "orange";
} else if (doc.order_release_date > frappe.datetime.get_today()) {
return "blue";
} else {
return "green";
}
});
},
set_query_filters(frm) {
frm.set_query("parent_warehouse", (doc) => {
return {
filters: {
is_group: 1,
company: doc.company,
},
};
});
},
get_actual_demand(frm) {
frm.call({
method: "get_actual_demand",
doc: frm.doc,
freeze: true,
freeze_message: __("Generating Master Production Schedule..."),
callback: (r) => {
frm.reload_doc();
},
});
},
set_custom_buttons(frm) {
if (!frm.is_new()) {
frm.add_custom_button(__("View MRP"), () => {
frappe.set_route("query-report", "Material Requirements Planning Report", {
company: frm.doc.company,
from_date: frm.doc.from_date,
to_date: frm.doc.to_date,
mps: frm.doc.name,
warehouse: frm.doc.parent_warehouse,
sales_forecast: frm.doc.sales_forecast,
});
});
}
},
get_sales_orders(frm) {
frm.sales_order_dialog = new frappe.ui.Dialog({
fields: [
{
fieldtype: "Section Break",
label: __("Filters for Sales Orders"),
},
{
fieldname: "customer",
fieldtype: "Link",
options: "Customer",
label: __("Customer"),
},
{
fieldtype: "Section Break",
},
{
fieldname: "from_date",
fieldtype: "Date",
label: __("From Date"),
},
{
fieldname: "to_date",
fieldtype: "Date",
label: __("To Date"),
},
{
fieldtype: "Column Break",
},
{
fieldname: "delivery_from_date",
fieldtype: "Date",
label: __("Delivery From Date"),
default: frm.doc.from_date,
},
{
fieldname: "delivery_to_date",
fieldtype: "Date",
label: __("Delivery To Date"),
},
],
title: __("Get Sales Orders"),
size: "large",
primary_action_label: __("Get Sales Orders"),
primary_action: (data) => {
frm.sales_order_dialog.hide();
frm.events.fetch_sales_orders(frm, data);
},
});
frm.sales_order_dialog.show();
},
fetch_sales_orders(frm, data) {
frm.call({
method: "fetch_sales_orders",
doc: frm.doc,
freeze: true,
freeze_message: __("Fetching Sales Orders..."),
args: data,
callback: (r) => {
frm.reload_doc();
},
});
},
get_material_requests(frm) {
frm.sales_order_dialog = new frappe.ui.Dialog({
fields: [
{
fieldtype: "Section Break",
label: __("Filters for Material Requests"),
},
{
fieldname: "material_request_type",
fieldtype: "Select",
label: __("Purpose"),
options: "\nPurchase\nManufacture",
default: "Manufacture",
},
{
fieldtype: "Column Break",
},
{
fieldname: "from_date",
fieldtype: "Date",
label: __("From Date"),
},
{
fieldname: "to_date",
fieldtype: "Date",
label: __("To Date"),
},
],
title: __("Get Material Requests"),
size: "large",
primary_action_label: __("Get Material Requests"),
primary_action: (data) => {
frm.sales_order_dialog.hide();
frm.events.fetch_materials_requests(frm, data);
},
});
frm.sales_order_dialog.show();
},
fetch_materials_requests(frm, data) {
frm.call({
method: "fetch_materials_requests",
doc: frm.doc,
freeze: true,
freeze_message: __("Fetching Material Requests..."),
args: data,
callback: (r) => {
frm.reload_doc();
},
});
},
});

View File

@@ -0,0 +1,264 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "naming_series:",
"creation": "2025-08-08 19:54:43.478386",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"mps_tab",
"naming_series",
"company",
"column_break_xdcy",
"posting_date",
"from_date",
"to_date",
"section_break_rrrx",
"column_break_klws",
"select_items",
"column_break_mtqw",
"parent_warehouse",
"sales_orders_and_material_requests_tab",
"open_orders_section",
"get_sales_orders",
"sales_orders",
"get_material_requests",
"material_requests",
"section_break_xtby",
"column_break_yhkr",
"column_break_vvys",
"get_actual_demand",
"section_break_cmgo",
"items",
"forecast_demand_section",
"sales_forecast",
"amended_from"
],
"fields": [
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "column_break_xdcy",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_rrrx",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_klws",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_mtqw",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 1,
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
"options": "Master Production Schedule Item"
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date",
"reqd": 1
},
{
"fieldname": "section_break_cmgo",
"fieldtype": "Section Break",
"label": "Actual Demand"
},
{
"default": "MPS.YY.-.######",
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "MPS.YY.-.######",
"reqd": 1
},
{
"description": "For projected and forecast quantities, the system will consider all child warehouses under the selected parent warehouse.",
"fieldname": "parent_warehouse",
"fieldtype": "Link",
"label": "Parent Warehouse",
"options": "Warehouse"
},
{
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date",
"reqd": 1
},
{
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date"
},
{
"fieldname": "sales_orders_and_material_requests_tab",
"fieldtype": "Tab Break",
"label": "Demand"
},
{
"fieldname": "sales_orders",
"fieldtype": "Table",
"label": "Sales Orders",
"options": "Production Plan Sales Order"
},
{
"fieldname": "mps_tab",
"fieldtype": "Tab Break",
"label": "Planning"
},
{
"fieldname": "material_requests",
"fieldtype": "Table",
"label": "Material Requests",
"options": "Production Plan Material Request"
},
{
"fieldname": "open_orders_section",
"fieldtype": "Section Break",
"label": "Open Orders"
},
{
"fieldname": "section_break_xtby",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_vvys",
"fieldtype": "Column Break"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Master Production Schedule",
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
{
"fieldname": "sales_forecast",
"fieldtype": "Link",
"label": "Sales Forecast",
"options": "Sales Forecast"
},
{
"fieldname": "get_sales_orders",
"fieldtype": "Button",
"label": "Get Sales Orders"
},
{
"fieldname": "get_material_requests",
"fieldtype": "Button",
"label": "Get Material Requests"
},
{
"fieldname": "select_items",
"fieldtype": "Table MultiSelect",
"label": "Select Items",
"options": "Master Production Schedule Item"
},
{
"fieldname": "column_break_yhkr",
"fieldtype": "Column Break"
},
{
"fieldname": "get_actual_demand",
"fieldtype": "Button",
"label": "Get Actual Demand"
},
{
"fieldname": "forecast_demand_section",
"fieldtype": "Section Break",
"label": "Forecast Demand"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-09-02 19:33:28.244544",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Master Production Schedule",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Manufacturing User",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Manufacturing Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock User",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,456 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import math
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
from frappe.utils import add_days, flt, getdate, parse_json, today
from frappe.utils.nestedset import get_descendants_of
class MasterProductionSchedule(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.manufacturing.doctype.master_production_schedule_item.master_production_schedule_item import (
MasterProductionScheduleItem,
)
from erpnext.manufacturing.doctype.production_plan_material_request.production_plan_material_request import (
ProductionPlanMaterialRequest,
)
from erpnext.manufacturing.doctype.production_plan_sales_order.production_plan_sales_order import (
ProductionPlanSalesOrder,
)
amended_from: DF.Link | None
company: DF.Link
from_date: DF.Date
items: DF.Table[MasterProductionScheduleItem]
material_requests: DF.Table[ProductionPlanMaterialRequest]
naming_series: DF.Literal["MPS.YY.-.######"]
parent_warehouse: DF.Link | None
posting_date: DF.Date
sales_forecast: DF.Link | None
sales_orders: DF.Table[ProductionPlanSalesOrder]
select_items: DF.TableMultiSelect[MasterProductionScheduleItem]
to_date: DF.Date | None
# end: auto-generated types
@frappe.whitelist()
def get_actual_demand(self):
self.set("items", [])
actual_demand_data = self.get_demand_data()
item_wise_data = self.get_item_wise_mps_data(actual_demand_data)
if not item_wise_data:
return []
self.update_item_details(item_wise_data)
self.add_mps_data(item_wise_data)
if not self.is_new():
self.save()
def validate(self):
self.set_to_date()
def set_to_date(self):
self.to_date = None
for row in self.items:
if not self.to_date or getdate(row.delivery_date) > getdate(self.to_date):
self.to_date = row.delivery_date
forecast_delivery_dates = self.get_sales_forecast_data()
for date in forecast_delivery_dates:
if not self.to_date or getdate(date) > getdate(self.to_date):
self.to_date = date
def get_sales_forecast_data(self):
if not self.sales_forecast:
return []
filters = {"parent": self.sales_forecast}
if self.select_items:
items = [d.item_code for d in self.select_items if d.item_code]
filters["item_code"] = ("in", items)
return frappe.get_all(
"Sales Forecast Item",
filters=filters,
pluck="delivery_date",
order_by="delivery_date asc",
)
def update_item_details(self, data):
items = [item[0] for item in data if item[0]]
item_details = self.get_item_details(items)
for key in data:
item_data = data[key]
item_code = key[0]
if item_code in item_details:
item_data.update(item_details[item_code])
def get_item_details(self, items):
doctype = frappe.qb.DocType("Item")
query = (
frappe.qb.from_(doctype)
.select(
doctype.name.as_("item_code"),
doctype.default_bom.as_("bom_no"),
doctype.item_name,
)
.where(doctype.name.isin(items))
)
item_details = query.run(as_dict=True)
item_wise_details = frappe._dict({})
if not item_details:
return item_wise_details
for row in item_details:
row.cumulative_lead_time = self.get_cumulative_lead_time(row.item_code, row.bom_no)
for row in item_details:
item_wise_details.setdefault(row.item_code, row)
return item_wise_details
def get_cumulative_lead_time(self, item_code, bom_no, time_in_days=0):
if not time_in_days:
time_in_days = get_item_lead_time(item_code)
bom_materials = frappe.get_all(
"BOM Item",
filters={"parent": bom_no, "docstatus": 1},
fields=["item_code", "bom_no"],
)
for row in bom_materials:
if row.bom_no:
time_in_days += self.get_cumulative_lead_time(row.item_code, row.bom_no)
else:
lead_time = get_item_lead_time(row.item_code)
time_in_days += lead_time
return time_in_days
def get_demand_data(self):
sales_order_data = self.get_sales_orders_data()
material_request_data = self.get_material_requests_data()
return sales_order_data + material_request_data
def get_material_requests_data(self):
if not self.material_requests:
return []
doctype = frappe.qb.DocType("Material Request Item")
query = (
frappe.qb.from_(doctype)
.select(
doctype.item_code,
doctype.warehouse,
doctype.stock_uom,
doctype.schedule_date.as_("delivery_date"),
doctype.parent.as_("material_request"),
doctype.stock_qty.as_("qty"),
)
.orderby(doctype.schedule_date)
)
if self.material_requests:
material_requests = [m.material_request for m in self.material_requests if m.material_request]
query = query.where(doctype.parent.isin(material_requests))
if self.from_date:
query = query.where(doctype.schedule_date >= self.from_date)
if self.to_date:
query = query.where(doctype.schedule_date <= self.to_date)
return query.run(as_dict=True)
def get_sales_orders_data(self):
sales_order_schedules = self.get_sales_order_schedules()
ignore_orders = []
if sales_order_schedules:
for row in sales_order_schedules:
if row.sales_order not in ignore_orders:
ignore_orders.append(row.sales_order)
sales_orders = self.get_items_from_sales_orders(ignore_orders)
return sales_orders + sales_order_schedules
def get_items_from_sales_orders(self, ignore_orders=None):
doctype = frappe.qb.DocType("Sales Order Item")
query = (
frappe.qb.from_(doctype)
.select(
doctype.item_code,
doctype.warehouse,
doctype.stock_uom,
doctype.delivery_date,
doctype.name.as_("sales_order"),
doctype.stock_qty.as_("qty"),
)
.where(doctype.docstatus == 1)
.orderby(doctype.delivery_date)
)
if self.from_date:
query = query.where(doctype.delivery_date >= self.from_date)
if self.to_date:
query = query.where(doctype.delivery_date <= self.to_date)
if self.sales_orders:
names = [s.sales_order for s in self.sales_orders if s.sales_order]
if ignore_orders:
names = [name for name in names if name not in ignore_orders]
query = query.where(doctype.parent.isin(names))
return query.run(as_dict=True)
def get_sales_order_schedules(self):
doctype = frappe.qb.DocType("Delivery Schedule Item")
query = frappe.qb.from_(doctype).select(
doctype.item_code,
doctype.warehouse,
doctype.stock_uom,
doctype.delivery_date,
doctype.sales_order,
doctype.stock_qty.as_("qty"),
)
if self.sales_orders:
names = [s.sales_order for s in self.sales_orders if s.sales_order]
query = query.where(doctype.sales_order.isin(names))
if self.from_date:
query = query.where(doctype.delivery_date >= self.from_date)
if self.to_date:
query = query.where(doctype.delivery_date <= self.to_date)
return query.run(as_dict=True)
def get_item_wise_mps_data(self, data):
item_wise_data = frappe._dict({})
for item in data:
key = (item.item_code, item.delivery_date)
if key not in item_wise_data:
item_wise_data[key] = frappe._dict(
{
"item_code": item.item_code,
"delivery_date": item.delivery_date,
"stock_uom": item.stock_uom,
"qty": 0.0,
"cumulative_lead_time": 0.0,
"order_release_date": item.delivery_date,
}
)
item_details = item_wise_data[key]
item_details.qty += item.qty
return item_wise_data
def add_mps_data(self, data):
data = frappe._dict(sorted(data.items(), key=lambda x: x[0][1]))
for key in data:
row = data[key]
row.cumulative_lead_time = math.ceil(row.cumulative_lead_time)
row.order_release_date = add_days(row.delivery_date, -row.cumulative_lead_time)
if getdate(row.order_release_date) < getdate(today()):
continue
row.planned_qty = row.qty
row.uom = row.stock_uom
row.warehouse = row.warehouse or self.parent_warehouse
self.append("items", row)
def get_distinct_items(self, data):
items = []
for item in data:
if item.item_code not in items:
items.append(item.item_code)
return items
@frappe.whitelist()
def fetch_materials_requests(self, **data):
if isinstance(data, str):
data = parse_json(data)
self.set("material_requests", [])
materials_requests = self.get_material_requests(data)
if not materials_requests:
frappe.msgprint(
_("No open Material Requests found for the given criteria."),
alert=True,
)
return
for row in materials_requests:
self.append(
"material_requests",
{
"material_request": row.name,
"material_request_date": row.transaction_date,
},
)
if not self.is_new():
self.save()
def get_material_requests(self, data):
doctype = frappe.qb.DocType("Material Request")
query = (
frappe.qb.from_(doctype)
.select(
doctype.name,
doctype.transaction_date,
)
.where((doctype.docstatus == 1) & (doctype.status.notin(["Closed", "Completed"])))
.orderby(doctype.schedule_date)
)
if data.get("material_request_type"):
query = query.where(doctype.material_request_type == data.get("material_request_type"))
if data.get("from_date"):
query = query.where(doctype.transaction_date >= data.get("from_date"))
if data.get("to_date"):
query = query.where(doctype.transaction_date <= data.get("to_date"))
if self.from_date:
query = query.where(doctype.schedule_date >= self.from_date)
if self.to_date:
query = query.where(doctype.schedule_date <= self.to_date)
return query.run(as_dict=True)
@frappe.whitelist()
def fetch_sales_orders(self, **data):
if isinstance(data, str):
data = parse_json(data)
self.set("sales_orders", [])
sales_orders = self.get_sales_orders(data)
if not sales_orders:
return
for row in sales_orders:
self.append(
"sales_orders",
{
"sales_order": row.name,
"sales_order_date": row.transaction_date,
"delivery_date": row.delivery_date,
"customer": row.customer,
"status": row.status,
"grand_total": row.grand_total,
},
)
if not self.is_new():
self.save()
def get_sales_orders(self, kwargs):
doctype = frappe.qb.DocType("Sales Order")
query = (
frappe.qb.from_(doctype)
.select(
doctype.name,
doctype.transaction_date,
doctype.delivery_date,
doctype.customer,
doctype.status,
doctype.grand_total,
)
.where((doctype.docstatus == 1) & (doctype.status.notin(["Closed", "Completed"])))
.orderby(doctype.delivery_date)
)
if kwargs.get("customer"):
query = query.where(doctype.customer == kwargs.get("customer"))
if kwargs.get("from_date"):
query = query.where(doctype.transaction_date >= kwargs.get("from_date"))
if kwargs.get("to_date"):
query = query.where(doctype.transaction_date <= kwargs.get("to_date"))
if kwargs.get("delivery_from_date"):
query = query.where(doctype.delivery_date >= kwargs.get("delivery_from_date"))
if kwargs.get("delivery_to_date"):
query = query.where(doctype.delivery_date <= kwargs.get("to_delivery_date"))
if items := self.get_items_for_mps():
doctype_item = frappe.qb.DocType("Sales Order Item")
query = query.join(doctype_item).on(doctype_item.parent == doctype.name)
query = query.where(doctype_item.item_code.isin(items))
return query.run(as_dict=True)
def get_items_for_mps(self):
if not self.select_items:
return
return [d.item_code for d in self.select_items if d.item_code]
def on_submit(self):
self.enqueue_mrp_creation()
def enqueue_mrp_creation(self):
frappe.enqueue_doc("Master Production Schedule", self.name, "make_mrp", queue="long", timeout=1800)
frappe.msgprint(
_("MRP Log documents are being created in the background."),
alert=True,
)
def get_item_lead_time(item_code):
doctype = frappe.qb.DocType("Item Lead Time")
query = (
frappe.qb.from_(doctype)
.select(
((doctype.manufacturing_time_in_mins / 1440) + doctype.purchase_time + doctype.buffer_time).as_(
"cumulative_lead_time"
)
)
.where(doctype.item_code == item_code)
)
result = query.run(as_dict=True)
if result:
return result[0].cumulative_lead_time or 0
return 0

View File

@@ -0,0 +1,20 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestMasterProductionSchedule(IntegrationTestCase):
"""
Integration tests for MasterProductionSchedule.
Use this class for testing interactions between multiple components.
"""
pass

View File

@@ -0,0 +1,101 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-08-12 17:09:08.171687",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_code",
"delivery_date",
"cumulative_lead_time",
"order_release_date",
"planned_qty",
"warehouse",
"item_name",
"bom_no",
"uom"
],
"fields": [
{
"columns": 2,
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Code",
"options": "Item",
"read_only": 1
},
{
"columns": 2,
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse",
"read_only": 1
},
{
"columns": 2,
"fieldname": "delivery_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Delivery Date",
"read_only": 1
},
{
"columns": 2,
"fieldname": "planned_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Planned Qty"
},
{
"columns": 2,
"fieldname": "order_release_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date"
},
{
"fieldname": "item_name",
"fieldtype": "Data",
"label": "Item Name",
"read_only": 1
},
{
"fieldname": "bom_no",
"fieldtype": "Link",
"in_list_view": 1,
"label": "BOM No",
"options": "BOM"
},
{
"fieldname": "uom",
"fieldtype": "Link",
"label": "UOM",
"options": "UOM"
},
{
"columns": 2,
"fieldname": "cumulative_lead_time",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Lead Time",
"read_only": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-09-02 19:41:27.167095",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Master Production Schedule Item",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,31 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class MasterProductionScheduleItem(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
bom_no: DF.Link | None
cumulative_lead_time: DF.Int
delivery_date: DF.Date | None
item_code: DF.Link | None
item_name: DF.Data | None
order_release_date: DF.Date | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
planned_qty: DF.Float
uom: DF.Link | None
warehouse: DF.Link | None
# end: auto-generated types
pass

View File

@@ -10,7 +10,8 @@
"sales_order_date",
"col_break1",
"customer",
"grand_total"
"grand_total",
"status"
],
"fields": [
{
@@ -58,17 +59,25 @@
"print_width": "120px",
"read_only": 1,
"width": "120px"
},
{
"fieldname": "status",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Status"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:20.746852",
"modified": "2025-08-21 15:16:13.828240",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Sales Order",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "ASC",
"states": []

View File

@@ -21,6 +21,7 @@ class ProductionPlanSalesOrder(Document):
parenttype: DF.Data
sales_order: DF.Link
sales_order_date: DF.Date | None
status: DF.Data | None
# end: auto-generated types
pass

View File

@@ -0,0 +1,56 @@
frappe.ui.form.on("Sales Forecast", {
refresh(frm) {
frm.trigger("set_query_filters");
frm.trigger("set_custom_buttons");
},
set_query_filters(frm) {
frm.set_query("parent_warehouse", (doc) => {
return {
filters: {
is_group: 1,
company: doc.company,
},
};
});
frm.set_query("item_code", "items", () => {
return {
filters: {
disabled: 0,
is_stock_item: 1,
},
};
});
},
generate_demand(frm) {
frm.call({
method: "generate_demand",
doc: frm.doc,
freeze: true,
callback: function (r) {
frm.reload_doc();
},
});
},
set_custom_buttons(frm) {
if (frm.doc.docstatus === 1 && frm.doc.status === "Planned") {
frm.add_custom_button(__("Create MPS"), () => {
frappe.model.open_mapped_doc({
method: "erpnext.manufacturing.doctype.sales_forecast.sales_forecast.create_mps",
frm: frm,
});
}).addClass("btn-primary");
}
},
});
frappe.ui.form.on("Sales Forecast Item", {
adjust_qty(frm, cdt, cdn) {
let row = locals[cdt][cdn];
row.demand_qty = row.forecast_qty + row.adjust_qty;
frappe.model.set_value(cdt, cdn, "demand_qty", row.demand_qty);
},
});

View File

@@ -0,0 +1,252 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "naming_series:",
"creation": "2025-08-12 17:20:16.012501",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"naming_series",
"company",
"posting_date",
"forecasting_method",
"column_break_xdcy",
"from_date",
"frequency",
"demand_number",
"section_break_rrrx",
"column_break_klws",
"selected_items",
"column_break_mtqw",
"parent_warehouse",
"section_break_cmgo",
"generate_demand",
"items",
"section_break_kuzf",
"amended_from",
"column_break_laqr",
"status",
"connections_tab"
],
"fields": [
{
"default": "SF.YY.-.######",
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "SF.YY.-.######",
"reqd": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "column_break_xdcy",
"fieldtype": "Column Break"
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date"
},
{
"collapsible": 1,
"fieldname": "section_break_rrrx",
"fieldtype": "Section Break",
"label": "Item and Warehouse"
},
{
"fieldname": "column_break_klws",
"fieldtype": "Column Break"
},
{
"fieldname": "selected_items",
"fieldtype": "Table MultiSelect",
"label": "Select Items",
"options": "Sales Forecast Item",
"reqd": 1
},
{
"fieldname": "column_break_mtqw",
"fieldtype": "Column Break"
},
{
"description": "For projected and forecast quantities, the system will consider all child warehouses under the selected parent warehouse.",
"fieldname": "parent_warehouse",
"fieldtype": "Link",
"label": "Parent Warehouse",
"options": "Warehouse",
"reqd": 1
},
{
"fieldname": "section_break_cmgo",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 1,
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
"options": "Sales Forecast Item"
},
{
"default": "Today",
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date",
"reqd": 1
},
{
"fieldname": "generate_demand",
"fieldtype": "Button",
"label": "Generate Demand"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Sales Forecast",
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
{
"default": "6",
"fieldname": "demand_number",
"fieldtype": "Int",
"label": "Number of Weeks / Months",
"reqd": 1
},
{
"fieldname": "connections_tab",
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
},
{
"fieldname": "section_break_kuzf",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_laqr",
"fieldtype": "Column Break"
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Planned\nMPS Generated",
"read_only": 1
},
{
"default": "Holt-Winters",
"fieldname": "forecasting_method",
"fieldtype": "Select",
"label": "Forecasting Method",
"options": "Holt-Winters\nManual"
},
{
"default": "Monthly",
"fieldname": "frequency",
"fieldtype": "Select",
"label": "Frequency",
"options": "Weekly\nMonthly",
"reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-09-21 13:24:34.720794",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Sales Forecast",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Manufacturing User",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Manufacturing Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,228 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
import pandas as pd
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import DateFormat, Sum, YearWeek
from frappe.utils import add_to_date, cint, date_diff, flt
from frappe.utils.nestedset import get_descendants_of
class SalesForecast(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.manufacturing.doctype.sales_forecast_item.sales_forecast_item import SalesForecastItem
amended_from: DF.Link | None
company: DF.Link
demand_number: DF.Int
forecasting_method: DF.Literal["Holt-Winters", "Manual"]
frequency: DF.Literal["Weekly", "Monthly"]
from_date: DF.Date
items: DF.Table[SalesForecastItem]
naming_series: DF.Literal["SF.YY.-.######"]
parent_warehouse: DF.Link
posting_date: DF.Date | None
selected_items: DF.TableMultiSelect[SalesForecastItem]
status: DF.Literal["Planned", "MPS Generated"]
# end: auto-generated types
def validate(self):
self.validate_demand_qty()
def validate_demand_qty(self):
if self.forecasting_method == "Manual":
return
for row in self.items:
demand_qty = row.forecast_qty + flt(row.adjust_qty)
if row.demand_qty != demand_qty:
row.demand_qty = demand_qty
def get_sales_data(self):
to_date = self.from_date
from_date = add_to_date(to_date, years=-3)
doctype = frappe.qb.DocType("Sales Order")
child_doctype = frappe.qb.DocType("Sales Order Item")
query = (
frappe.qb.from_(doctype)
.inner_join(child_doctype)
.on(child_doctype.parent == doctype.name)
.select(child_doctype.item_code, Sum(child_doctype.qty).as_("qty"), doctype.transaction_date)
.where((doctype.docstatus == 1) & (doctype.transaction_date.between(from_date, to_date)))
.groupby(child_doctype.item_code)
)
if self.selected_items:
items = [item.item_code for item in self.selected_items]
query = query.where(child_doctype.item_code.isin(items))
if self.parent_warehouse:
warehouses = get_descendants_of("Warehouse", self.parent_warehouse)
query = query.where(child_doctype.warehouse.isin(warehouses))
query = query.groupby(doctype.transaction_date)
return query.run(as_dict=True)
def generate_manual_demand(self):
forecast_demand = []
for row in self.selected_items:
item_details = frappe.db.get_value(
"Item", row.item_code, ["item_name", "stock_uom as uom"], as_dict=True
)
for index in range(self.demand_number):
if self.horizon_type == "Monthly":
delivery_date = add_to_date(self.from_date, months=index + 1)
else:
delivery_date = add_to_date(self.from_date, weeks=index + 1)
forecast_demand.append(
{
"item_code": row.item_code,
"delivery_date": delivery_date,
"item_name": item_details.item_name,
"uom": item_details.uom,
"demand_qty": 0.0,
}
)
for demand in forecast_demand:
self.append("items", demand)
self.save()
@frappe.whitelist()
def generate_demand(self):
from statsmodels.tsa.holtwinters import ExponentialSmoothing
self.set("items", [])
if self.forecasting_method == "Manual":
self.generate_manual_demand()
return
sales_data = self.get_sales_data()
if not sales_data:
frappe.throw(_("No sales data found for the selected items."))
itemwise_data = self.group_sales_data_by_item(sales_data)
for item_code, data in itemwise_data.items():
seasonal_periods = self.get_seasonal_periods(data)
pd_sales_data = pd.DataFrame({"item": data.item, "date": data.date, "qty": data.qty})
resample_val = "M" if self.horizon_type == "Monthly" else "W"
_sales_data = pd_sales_data.set_index("date").resample(resample_val).sum()["qty"]
model = ExponentialSmoothing(
_sales_data, trend="add", seasonal="mul", seasonal_periods=seasonal_periods
)
fit = model.fit()
forecast = fit.forecast(self.demand_number)
forecast_data = forecast.to_dict()
if forecast_data:
self.add_sales_forecast_item(item_code, forecast_data)
self.save()
def add_sales_forecast_item(self, item_code, forecast_data):
item_details = frappe.db.get_value(
"Item", item_code, ["item_name", "stock_uom as uom", "name as item_code"], as_dict=True
)
uom_whole_number = frappe.get_cached_value("UOM", item_details.uom, "must_be_whole_number")
for date, qty in forecast_data.items():
if uom_whole_number:
qty = round(qty)
item_details.update(
{
"delivery_date": date,
"forecast_qty": qty,
"demand_qty": qty,
"warehouse": self.parent_warehouse,
}
)
self.append("items", item_details)
def get_seasonal_periods(self, data):
days = date_diff(data["end_date"], data["start_date"])
if self.horizon_type == "Monthly":
months = (days / 365) * 12
seasonal_periods = cint(months / 2)
if seasonal_periods > 12:
seasonal_periods = 12
else:
weeks = days / 7
seasonal_periods = cint(weeks / 2)
if seasonal_periods > 52:
seasonal_periods = 52
return seasonal_periods
def group_sales_data_by_item(self, sales_data):
"""
Group sales data by item code and calculate total quantity sold.
"""
itemwise_data = frappe._dict({})
for row in sales_data:
if row.item_code not in itemwise_data:
itemwise_data[row.item_code] = frappe._dict(
{
"start_date": row.transaction_date,
"item": [],
"date": [],
"qty": [],
"end_date": "",
}
)
item_data = itemwise_data[row.item_code]
item_data["item"].append(row.item_code)
item_data["date"].append(pd.to_datetime(row.transaction_date))
item_data["qty"].append(row.qty)
item_data["end_date"] = row.transaction_date
return itemwise_data
@frappe.whitelist()
def create_mps(source_name, target_doc=None):
def postprocess(source, doc):
doc.naming_series = "MPS.YY.-.######"
doc = get_mapped_doc(
"Sales Forecast",
source_name,
{
"Sales Forecast": {
"doctype": "Master Production Schedule",
"validation": {"docstatus": ["=", 1]},
"field_map": {
"name": "sales_forecast",
"from_date": "from_date",
},
},
},
target_doc,
postprocess,
)
return doc

View File

@@ -0,0 +1,13 @@
from frappe import _
def get_data():
return {
"fieldname": "demand_planning",
"transactions": [
{
"label": _("MPS"),
"items": ["Master Production Schedule"],
},
],
}

View File

@@ -0,0 +1,12 @@
frappe.listview_settings["Sales Forecast"] = {
add_fields: ["status"],
get_indicator: function (doc) {
if (doc.status === "Planned") {
// Closed
return [__("Planned"), "orange", "status,=,Planned"];
} else if (doc.status === "MPS Generated") {
// on hold
return [__("MPS Generated"), "green", "status,=,MPS Generated"];
}
},
};

View File

@@ -0,0 +1,20 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestSalesForecast(IntegrationTestCase):
"""
Integration tests for SalesForecast.
Use this class for testing interactions between multiple components.
"""
pass

View File

@@ -0,0 +1,107 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-08-18 20:57:19.816490",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_code",
"item_name",
"uom",
"delivery_date",
"forecast_qty",
"adjust_qty",
"demand_qty",
"warehouse"
],
"fields": [
{
"columns": 2,
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Code",
"options": "Item",
"read_only": 1,
"reqd": 1
},
{
"columns": 2,
"fetch_from": "item_code.item_name",
"fetch_if_empty": 1,
"fieldname": "item_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Item Name",
"read_only": 1
},
{
"columns": 2,
"fetch_from": "item_code.sales_uom",
"fetch_if_empty": 1,
"fieldname": "uom",
"fieldtype": "Link",
"in_list_view": 1,
"label": "UOM",
"options": "UOM",
"read_only": 1
},
{
"columns": 2,
"fieldname": "delivery_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Delivery Date",
"read_only": 1
},
{
"columns": 2,
"fieldname": "forecast_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Forecast Qty",
"non_negative": 1,
"read_only": 1
},
{
"columns": 2,
"fieldname": "adjust_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Adjust Qty"
},
{
"columns": 3,
"fieldname": "demand_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Demand Qty",
"non_negative": 1,
"reqd": 1
},
{
"columns": 2,
"fieldname": "warehouse",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Warehouse",
"options": "Warehouse",
"read_only": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-08-18 21:59:38.859082",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Sales Forecast Item",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,30 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class SalesForecastItem(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
adjust_qty: DF.Float
delivery_date: DF.Date | None
demand_qty: DF.Float
forecast_qty: DF.Float
item_code: DF.Link
item_name: DF.Data | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
uom: DF.Link | None
warehouse: DF.Link | None
# end: auto-generated types
pass

View File

@@ -14,6 +14,7 @@
"item_name",
"image",
"bom_no",
"mps",
"sales_order",
"column_break1",
"company",
@@ -601,6 +602,13 @@
"label": "Disassembled Qty",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "mps",
"fieldtype": "Link",
"label": "MPS",
"options": "Master Production Schedule",
"read_only": 1
}
],
"grid_page_length": 50,
@@ -609,7 +617,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2025-06-21 00:55:45.916224",
"modified": "2025-08-28 11:01:48.719824",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",

View File

@@ -101,6 +101,7 @@ class WorkOrder(Document):
material_request: DF.Link | None
material_request_item: DF.Data | None
material_transferred_for_manufacturing: DF.Float
mps: DF.Link | None
naming_series: DF.Literal["MFG-WO-.YYYY.-"]
operations: DF.Table[WorkOrderOperation]
planned_end_date: DF.Datetime | None

View File

@@ -390,10 +390,18 @@ def get_time_logs(job_cards):
@frappe.whitelist()
def get_default_holiday_list():
return frappe.get_cached_value(
"Company", frappe.defaults.get_user_default("Company"), "default_holiday_list"
)
def get_default_holiday_list(company=None):
if company:
if not frappe.has_permission("Company", "read"):
return []
if not frappe.db.exists("Company", company):
return []
if not company:
company = frappe.defaults.get_user_default("Company")
return frappe.get_cached_value("Company", company, "default_holiday_list")
def check_if_within_operating_hours(workstation, operation, from_datetime, to_datetime):

View File

@@ -0,0 +1,170 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.query_reports["Material Requirements Planning Report"] = {
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1,
},
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
default: frappe.datetime.add_days(frappe.datetime.get_today(), 7),
reqd: 1,
},
{
fieldname: "to_date",
label: __("To Date"),
fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.get_today(), 3),
reqd: 1,
},
{
fieldname: "item_code",
label: __("Item Code"),
fieldtype: "Link",
options: "Item",
get_query: function () {
return {
filters: {
is_stock_item: 1,
},
};
},
},
{
fieldname: "warehouse",
label: __("Warehouse"),
fieldtype: "Link",
options: "Warehouse",
reqd: 1,
default: frappe.defaults.get_user_default("Warehouse"),
},
{
fieldname: "mps",
label: __("MPS"),
fieldtype: "Link",
options: "Master Production Schedule",
reqd: 1,
},
{
fieldname: "type_of_material",
label: __("Type of Material"),
fieldtype: "Select",
default: "All",
options: "\nFinished Goods\nRaw Materials\nAll",
},
{
fieldname: "safety_stock_check_frequency",
label: __("Safety Stock Check Frequency"),
fieldtype: "Select",
default: "Weekly",
options: "\nDaily\nWeekly\nMonthly",
},
{
fieldname: "add_safety_stock",
label: __("Add Safety Stock"),
fieldtype: "Check",
default: 1,
},
{
fieldname: "show_in_bucket_view",
label: __("Show in Bucket View"),
fieldtype: "Check",
},
{
fieldname: "bucket_view",
label: __("View Data Based on"),
fieldtype: "Select",
options: "Delivery Date\nRelease Date",
default: "Delivery Date",
depends_on: "eval:doc.show_in_bucket_view == 1",
},
{
fieldname: "bucket_size",
label: __("Bucket Size"),
fieldtype: "Select",
default: "Monthly",
options: "Daily\nWeekly\nMonthly",
depends_on: "eval:doc.show_in_bucket_view == 1",
},
],
formatter: function (value, row, column, data, default_formatter) {
if (column.fieldtype === "Float" && !data.item_code) {
return "";
}
value = default_formatter(value, row, column, data);
// if (column.fieldname === "release_date") {
// if (frappe.datetime.get_day_diff(data.release_date, frappe.datetime.get_today()) < 0) {
// return `<span class="text-danger">${value}</span>`;
// }
// }
return value;
},
get_datatable_options(options) {
return Object.assign(options, {
checkboxColumn: true,
});
},
onload(report) {
report.page.add_inner_button(__("Make Purchase / Work Order"), () => {
let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
let selected_rows = indexes.map((i) => frappe.query_report.data[i]);
if (!selected_rows.length) {
frappe.throw(__("Please select a row to create a Reposting Entry"));
} else {
let show_in_bucket_view = frappe.query_report.get_filter_value("show_in_bucket_view");
if (show_in_bucket_view) {
frappe.throw(__("Please uncheck 'Show in Bucket View' to create Orders"));
}
frappe.prompt(
[
{
fieldname: "use_default_warehouse",
label: __("Use Default Warehouse"),
fieldtype: "Check",
default: 1,
},
{
fieldname: "warehouse",
label: __("Warehouse"),
fieldtype: "Link",
options: "Warehouse",
depends_on: "eval:!doc.use_default_warehouse",
mandatory_depends_on: "eval:!doc.use_default_warehouse",
},
],
(prompt_data) => {
frappe.call({
method: "erpnext.manufacturing.report.material_requirements_planning_report.material_requirements_planning_report.make_order",
freeze: true,
args: {
selected_rows: selected_rows,
company: frappe.query_report.get_filter_value("company"),
warehouse: !prompt_data.use_default_warehouse ? prompt_data.warehouse : null,
mps: frappe.query_report.get_filter_value("mps"),
},
callback: function (r) {
if (r.message) {
frappe.set_route("List", r.message);
}
},
});
}
);
}
});
},
};

View File

@@ -0,0 +1,47 @@
{
"add_total_row": 0,
"add_translate_data": 0,
"columns": [],
"creation": "2025-08-15 14:29:09.655112",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"json": "{}",
"letterhead": null,
"modified": "2025-08-22 17:55:48.952251",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Material Requirements Planning Report",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Master Production Schedule",
"report_name": "Material Requirements Planning Report",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "Manufacturing Manager"
},
{
"role": "Manufacturing User"
},
{
"role": "Purchase Manager"
},
{
"role": "Purchase User"
},
{
"role": "Stock User"
},
{
"role": "Stock Manager"
}
],
"timeout": 0
}

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Delivery Schedule Item", {
// refresh(frm) {
// },
// });

View File

@@ -0,0 +1,157 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-08-21 16:55:39.222786",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"item_code",
"warehouse",
"column_break_sfve",
"delivery_date",
"sales_order",
"sales_order_item",
"section_break_gttb",
"qty",
"uom",
"conversion_factor",
"column_break_gsks",
"stock_qty",
"stock_uom"
],
"fields": [
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": "Item Code",
"options": "Item",
"read_only": 1
},
{
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Qty",
"read_only": 1
},
{
"fieldname": "conversion_factor",
"fieldtype": "Float",
"label": "Conversion Factor",
"read_only": 1
},
{
"fieldname": "column_break_sfve",
"fieldtype": "Column Break"
},
{
"fieldname": "stock_qty",
"fieldtype": "Float",
"label": "Stock Qty",
"read_only": 1
},
{
"fieldname": "delivery_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Delivery Date",
"read_only": 1
},
{
"fieldname": "sales_order",
"fieldtype": "Link",
"label": "Sales Order",
"options": "Sales Order",
"read_only": 1,
"search_index": 1
},
{
"fieldname": "sales_order_item",
"fieldtype": "Data",
"label": "Sales Order Item",
"read_only": 1,
"search_index": 1
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse",
"read_only": 1
},
{
"fieldname": "section_break_gttb",
"fieldtype": "Section Break"
},
{
"fieldname": "uom",
"fieldtype": "Link",
"in_list_view": 1,
"label": "UOM",
"options": "UOM",
"read_only": 1
},
{
"fieldname": "column_break_gsks",
"fieldtype": "Column Break"
},
{
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
"options": "UOM",
"read_only": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-08-21 18:11:30.134073",
"modified_by": "Administrator",
"module": "Selling",
"name": "Delivery Schedule Item",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "item_code"
}

View File

@@ -0,0 +1,29 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class DeliveryScheduleItem(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
conversion_factor: DF.Float
delivery_date: DF.Date | None
item_code: DF.Link | None
qty: DF.Float
sales_order: DF.Link | None
sales_order_item: DF.Data | None
stock_qty: DF.Float
stock_uom: DF.Link | None
uom: DF.Link | None
warehouse: DF.Link | None
# end: auto-generated types
pass

View File

@@ -0,0 +1,20 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestDeliveryScheduleItem(IntegrationTestCase):
"""
Integration tests for DeliveryScheduleItem.
Use this class for testing interactions between multiple components.
"""
pass

View File

@@ -212,6 +212,7 @@ frappe.ui.form.on("Sales Order", {
"Purchase Order",
"Unreconcile Payment",
"Unreconcile Payment Entries",
"Delivery Schedule Item",
];
},
@@ -545,6 +546,209 @@ frappe.ui.form.on("Sales Order", {
};
frappe.set_route("query-report", "Reserved Stock");
},
prepare_delivery_schedule(frm, row, data) {
let fields = [
{
fieldtype: "Date",
fieldname: "delivery_date",
label: __("First Delivery Date"),
reqd: 1,
default: row.delivery_date || frm.doc.delivery_date || frappe.datetime.get_today(),
},
{
fieldtype: "Float",
fieldname: "qty",
label: __("Qty"),
read_only: 1,
default: row.qty || 0,
},
{
fieldtype: "Column Break",
},
{
fieldtype: "Select",
fieldname: "frequency",
label: __("Frequency"),
options: "\nWeekly\nMonthly\nQuarterly\nHalf Yearly\nYearly",
},
{
fieldtype: "Int",
fieldname: "no_of_deliveries",
label: __("No of Deliveries"),
},
{
fieldtype: "Section Break",
},
{
fieldtype: "Button",
fieldname: "get_delivery_schedule",
label: __("Get Delivery Schedule"),
click: () => {
frappe.db.get_value("UOM", row.uom, "must_be_whole_number", (r) => {
frm.events.add_delivery_schedule(frm, row, r.must_be_whole_number);
});
},
},
{
fieldtype: "Table",
data: [],
fieldname: "delivery_schedule",
label: __("Delivery Schedule"),
fields: [
{
fieldtype: "Date",
fieldname: "delivery_date",
label: __("Delivery Date"),
reqd: 1,
in_list_view: 1,
},
{
fieldtype: "Float",
fieldname: "qty",
label: __("Qty"),
reqd: 1,
in_list_view: 1,
},
{
fieldtype: "Data",
fieldname: "Name",
label: __("name"),
read_only: 1,
},
],
},
];
frm.schedule_dialog = new frappe.ui.Dialog({
title: __("Delivery Schedule"),
fields: fields,
size: "large",
primary_action_label: __("Add Schedule"),
primary_action: (data) => {
if (!data.delivery_schedule || !data.delivery_schedule.length) {
frappe.throw(__("Please enter at least one delivery date and quantity"));
}
let total_qty = 0;
data.delivery_schedule.forEach((d) => {
if (!d.qty) {
frappe.throw(__("Please enter a valid quantity"));
}
total_qty += flt(d.qty);
});
if (total_qty > flt(row.qty)) {
frappe.throw(
__("Total quantity in delivery schedule cannot be greater than the item quantity")
);
}
frappe.call({
doc: frm.doc,
method: "create_delivery_schedule",
args: {
child_row: row,
schedules: data.delivery_schedule,
},
freeze: true,
freeze_message: __("Creating Delivery Schedule..."),
callback: function () {
frm.refresh_field("items");
frm.schedule_dialog.hide();
},
});
},
});
frm.schedule_dialog.show();
if (data?.length) {
data.forEach((d) => {
if (d.delivery_date && d.qty) {
frm.schedule_dialog.fields_dict.delivery_schedule.df.data.push({
delivery_date: d.delivery_date,
qty: d.qty,
name: d.name,
});
}
});
frm.schedule_dialog.fields_dict.delivery_schedule.refresh();
}
},
add_delivery_schedule(frm, row, must_be_whole_number) {
let first_delivery_date = frm.schedule_dialog.get_value("delivery_date");
let frequency = frm.schedule_dialog.get_value("frequency");
let no_of_deliveries = cint(frm.schedule_dialog.get_value("no_of_deliveries"));
if (!frequency) {
frappe.throw(__("Please select a frequency for delivery schedule"));
}
if (!first_delivery_date) {
frappe.throw(__("Please enter the first delivery date"));
}
if (no_of_deliveries <= 0) {
frappe.throw(__("Please enter a valid number of deliveries"));
}
frm.schedule_dialog.fields_dict.delivery_schedule.df.data = [];
let qty_to_deliver = row.qty;
let qty_per_delivery = qty_to_deliver / no_of_deliveries;
for (let i = 0; i < no_of_deliveries; i++) {
let qty = qty_per_delivery;
if (must_be_whole_number) {
qty = cint(qty);
}
if (i === no_of_deliveries - 1) {
// Last delivery, adjust the quantity to deliver the remaining amount
qty = qty_to_deliver;
qty_to_deliver = 0;
} else {
qty_to_deliver -= qty;
}
frm.schedule_dialog.fields_dict.delivery_schedule.df.data.push({
delivery_date: first_delivery_date,
qty: qty,
});
if (frequency === "Weekly") {
first_delivery_date = frappe.datetime.add_days(first_delivery_date, i + 1 * 7);
} else {
let month_mapper = {
Monthly: 1,
Quarterly: 3,
Half_Yearly: 6,
Yearly: 12,
};
first_delivery_date = frappe.datetime.add_months(
first_delivery_date,
month_mapper[frequency] * i + 1
);
}
}
frm.schedule_dialog.fields_dict.delivery_schedule.refresh();
},
set_delivery_schedule(frm, row, data) {
data.forEach((d) => {
if (d.delivery_date && d.qty) {
frm.schedule_dialog.fields_dict.delivery_schedule.df.data.push({
delivery_date: d.delivery_date,
qty: d.qty,
});
}
});
frm.schedule_dialog.fields_dict.delivery_schedule.refresh();
},
});
frappe.ui.form.on("Sales Order Item", {
@@ -557,11 +761,27 @@ frappe.ui.form.on("Sales Order Item", {
frm.script_manager.copy_from_first_row("items", row, ["delivery_date"]);
}
},
delivery_date: function (frm, cdt, cdn) {
if (!frm.doc.delivery_date) {
erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "items", "delivery_date");
}
},
add_schedule(frm, cdt, cdn) {
let row = locals[cdt][cdn];
frappe.call({
method: "get_delivery_schedule",
doc: frm.doc,
args: {
sales_order_item: row.name,
},
callback: function (r) {
frm.events.prepare_delivery_schedule(frm, row, r.message);
},
});
},
});
erpnext.selling.SalesOrderController = class SalesOrderController extends erpnext.selling.SellingController {

View File

@@ -13,7 +13,7 @@ from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import Sum
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, nowdate, strip_html
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, nowdate, parse_json, strip_html
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
unlink_inter_company_doc,
@@ -467,6 +467,7 @@ class SalesOrder(SellingController):
if self.status == "Closed":
frappe.throw(_("Closed order cannot be cancelled. Unclose to cancel."))
self.delete_delivery_schedule_items()
self.check_nextdoc_docstatus()
self.update_reserved_qty()
self.update_project()
@@ -791,6 +792,79 @@ class SalesOrder(SellingController):
if not item.delivery_date:
item.delivery_date = self.delivery_date
@frappe.whitelist()
def get_delivery_schedule(self, sales_order_item):
return frappe.get_all(
"Delivery Schedule Item",
filters={"sales_order_item": sales_order_item, "sales_order": self.name},
fields=["delivery_date", "qty", "name"],
order_by="delivery_date asc",
)
@frappe.whitelist()
def create_delivery_schedule(self, child_row, schedules):
if isinstance(child_row, dict):
child_row = frappe._dict(child_row)
if isinstance(schedules, str):
schedules = parse_json(schedules)
names = []
first_delivery_date = None
for row in schedules:
row = frappe._dict(row)
if not first_delivery_date:
first_delivery_date = row.delivery_date
data = {
"delivery_date": row.delivery_date,
"qty": row.qty,
"uom": child_row.uom,
"stock_uom": child_row.stock_uom,
"item_code": child_row.item_code,
"conversion_factor": child_row.conversion_factor or 1.0,
"warehouse": child_row.warehouse,
"sales_order_item": child_row.name,
"sales_order": self.name,
"stock_qty": row.qty * (child_row.conversion_factor or 1.0),
}
if frappe.db.exists("Delivery Schedule Item", row.name):
doc = frappe.get_doc("Delivery Schedule Item", row.name)
else:
doc = frappe.new_doc("Delivery Schedule Item")
doc.update(data)
doc.save(ignore_permissions=True)
names.append(doc.name)
if names:
self.delete_delivery_schedule_items(names)
if first_delivery_date:
self.update_delivery_date_based_on_schedule(child_row, first_delivery_date)
def update_delivery_date_based_on_schedule(self, child_row, first_delivery_date):
for row in self.items:
if row.name == child_row.name:
if first_delivery_date:
row.delivery_date = first_delivery_date
break
self.save()
def delete_delivery_schedule_items(self, ignore_names=None):
"""Delete delivery schedule items."""
doctype = frappe.qb.DocType("Delivery Schedule Item")
query = frappe.qb.from_(doctype).delete().where(doctype.sales_order == self.name)
if ignore_names:
query = query.where(doctype.name.notin(ignore_names))
query.run()
def get_unreserved_qty(item: object, reserved_qty_details: dict) -> float:
"""Returns the unreserved quantity for the Sales Order Item."""

View File

@@ -29,5 +29,6 @@ def get_data():
{"label": _("Manufacturing"), "items": ["Work Order", "BOM", "Blanket Order"]},
{"label": _("Reference"), "items": ["Quotation", "Auto Repeat", "Stock Reservation Entry"]},
{"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]},
{"label": _("Schedule"), "items": ["Delivery Schedule Item"]},
],
}

View File

@@ -83,6 +83,8 @@
"actual_qty",
"column_break_jpky",
"company_total_stock",
"sales_order_schedule_section",
"add_schedule",
"manufacturing_section_section",
"bom_no",
"planning_section",
@@ -965,18 +967,29 @@
"label": "Project",
"options": "Project",
"search_index": 1
},
{
"fieldname": "sales_order_schedule_section",
"fieldtype": "Section Break",
"label": "Sales Order Schedule"
},
{
"fieldname": "add_schedule",
"fieldtype": "Button",
"label": "Add Schedule"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-02-28 09:45:43.934947",
"modified": "2025-08-21 17:01:54.269105",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],

View File

@@ -65,7 +65,7 @@
"icon": "fa fa-compass",
"idx": 1,
"links": [],
"modified": "2024-03-27 13:10:57.375141",
"modified": "2025-08-21 18:59:27.900209",
"modified_by": "Administrator",
"module": "Setup",
"name": "UOM",
@@ -98,9 +98,28 @@
"read": 1,
"report": 1,
"role": "Stock User"
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "ASC",

View File

@@ -517,6 +517,16 @@ $.extend(erpnext.item, {
},
__("Actions")
);
frm.add_custom_button(
__("Make Lead Time"),
function () {
frm.make_new("Item Lead Time", {
item_code: frm.doc.name,
});
},
__("Actions")
);
},
weight_to_validate: function (frm) {

View File

@@ -116,7 +116,15 @@
"customer_code",
"default_item_manufacturer",
"default_manufacturer_part_no",
"total_projected_qty"
"total_projected_qty",
"lead_time_in_days_section",
"procurement_time",
"manufacturing_time",
"column_break_whvr",
"planning_buffer",
"cumulative_time",
"capacity_in_days_section",
"production_capacity"
],
"fields": [
{
@@ -888,6 +896,46 @@
"fieldname": "deferred_accounting_section",
"fieldtype": "Section Break",
"label": "Deferred Accounting"
},
{
"fieldname": "lead_time_in_days_section",
"fieldtype": "Section Break",
"label": "Lead Time (In Days)"
},
{
"fieldname": "procurement_time",
"fieldtype": "Int",
"label": "Procurement Time"
},
{
"fieldname": "planning_buffer",
"fieldtype": "Int",
"label": "Planning Buffer"
},
{
"fieldname": "column_break_whvr",
"fieldtype": "Column Break"
},
{
"fieldname": "manufacturing_time",
"fieldtype": "Int",
"label": "Manufacturing Time"
},
{
"fieldname": "cumulative_time",
"fieldtype": "Int",
"label": "Cumulative Time",
"read_only": 1
},
{
"fieldname": "capacity_in_days_section",
"fieldtype": "Section Break",
"label": "Capacity (In Days)"
},
{
"fieldname": "production_capacity",
"fieldtype": "Int",
"label": "Production Capacity"
}
],
"icon": "fa fa-tag",
@@ -895,7 +943,7 @@
"image_field": "image",
"links": [],
"make_attachments_public": 1,
"modified": "2025-08-08 14:58:48.674193",
"modified": "2025-08-14 23:35:56.293048",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -81,6 +81,7 @@ class Item(Document):
brand: DF.Link | None
country_of_origin: DF.Link | None
create_new_batch: DF.Check
cumulative_time: DF.Int
customer: DF.Link | None
customer_code: DF.SmallText | None
customer_items: DF.Table[ItemCustomerDetail]
@@ -119,6 +120,7 @@ class Item(Document):
item_name: DF.Data | None
last_purchase_rate: DF.Float
lead_time_days: DF.Int
manufacturing_time: DF.Int
max_discount: DF.Float
min_order_qty: DF.Float
naming_series: DF.Literal["STO-ITEM-.YYYY.-"]
@@ -127,6 +129,9 @@ class Item(Document):
opening_stock: DF.Float
over_billing_allowance: DF.Float
over_delivery_receipt_allowance: DF.Float
planning_buffer: DF.Int
procurement_time: DF.Int
production_capacity: DF.Int
purchase_uom: DF.Link | None
quality_inspection_template: DF.Link | None
reorder_levels: DF.Table[ItemReorder]
@@ -216,10 +221,16 @@ class Item(Document):
self.validate_auto_reorder_enabled_in_stock_settings()
self.cant_change()
self.validate_item_tax_net_rate_range()
self.set_cumulative_time()
if not self.is_new():
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
def set_cumulative_time(self):
self.cumulative_time = (
cint(self.procurement_time) + cint(self.manufacturing_time) + cint(self.planning_buffer)
)
def on_update(self):
self.update_variants()
self.update_item_price()

View File

@@ -32,5 +32,6 @@ def get_data():
{"label": _("Manufacture"), "items": ["Production Plan", "Work Order", "Item Manufacturer"]},
{"label": _("Traceability"), "items": ["Serial No", "Batch"]},
{"label": _("Stock Movement"), "items": ["Stock Entry", "Stock Reconciliation"]},
{"label": _("Lead Time"), "items": ["Item Lead Time"]},
],
}

View File

@@ -0,0 +1,65 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Item Lead Time", {
refresh(frm) {
frm.trigger("setup_queries");
},
setup_queries(frm) {
frm.set_query("bom_no", () => {
return {
filters: {
item: frm.doc.item_code,
docstatus: 1,
with_operations: 1,
},
};
});
},
shift_time_in_hours(frm) {
frm.trigger("calculate_total_workstation_time");
},
no_of_workstations(frm) {
frm.trigger("calculate_total_workstation_time");
},
no_of_shift(frm) {
frm.trigger("calculate_total_workstation_time");
},
calculate_total_workstation_time(frm) {
let total_workstation_time =
frm.doc.shift_time_in_hours * frm.doc.no_of_workstations * frm.doc.no_of_shift;
frm.set_value("total_workstation_time", total_workstation_time);
},
total_workstation_time(frm) {
frm.trigger("calculate_no_of_units_produced");
},
manufacturing_time_in_mins(frm) {
frm.trigger("calculate_no_of_units_produced");
},
calculate_no_of_units_produced(frm) {
let no_of_units_produced =
Math.ceil(frm.doc.total_workstation_time / frm.doc.manufacturing_time_in_mins) * 60;
frm.set_value("no_of_units_produced", no_of_units_produced);
},
no_of_units_produced(frm) {
frm.trigger("calculate_capacity_per_day");
},
daily_yield(frm) {
frm.trigger("calculate_capacity_per_day");
},
calculate_capacity_per_day(frm) {
let capacity_per_day = (frm.doc.daily_yield * frm.doc.no_of_units_produced) / 100;
frm.set_value("capacity_per_day", Math.ceil(capacity_per_day));
},
});

View File

@@ -0,0 +1,216 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:item_code",
"creation": "2025-08-21 12:46:54.727571",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"manufacturing_time_tab",
"item_code",
"column_break_qwyo",
"manufacturing_section",
"bom_no",
"shift_time_in_hours",
"no_of_workstations",
"column_break_cdqv",
"no_of_shift",
"total_workstation_time",
"section_break_wuqi",
"column_break_yilv",
"manufacturing_time_in_mins",
"no_of_days",
"no_of_units_produced",
"column_break_bbsv",
"daily_yield",
"capacity_per_day",
"purchase_lead_time_tab",
"section_break_fwyn",
"purchase_and_other_column",
"purchase_time",
"column_break_lsfp",
"buffer_time",
"item_details_tab",
"item_name",
"stock_uom"
],
"fields": [
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": "Item Code",
"options": "Item",
"unique": 1
},
{
"fieldname": "column_break_qwyo",
"fieldtype": "Column Break"
},
{
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"label": "Item Name",
"read_only": 1
},
{
"fieldname": "section_break_fwyn",
"fieldtype": "Section Break",
"label": "Purchase"
},
{
"fieldname": "purchase_and_other_column",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_lsfp",
"fieldtype": "Column Break"
},
{
"description": "In Days",
"fieldname": "buffer_time",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Buffer Time"
},
{
"fieldname": "manufacturing_section",
"fieldtype": "Section Break",
"label": "Workstation"
},
{
"fieldname": "bom_no",
"fieldtype": "Link",
"label": "Default BOM",
"options": "BOM"
},
{
"default": "1",
"fieldname": "no_of_shift",
"fieldtype": "Int",
"label": "No of Shift"
},
{
"fieldname": "column_break_bbsv",
"fieldtype": "Column Break"
},
{
"description": "Per Unit Time in Mins",
"fieldname": "manufacturing_time_in_mins",
"fieldtype": "Int",
"label": "Manufacturing Time"
},
{
"description": "Per Day",
"fieldname": "total_workstation_time",
"fieldtype": "Int",
"label": "Total Workstation Time (In Hours)"
},
{
"default": "90",
"description": "(Good Units Produced / Total Units Produced) \u00d7 100",
"fieldname": "daily_yield",
"fieldtype": "Percent",
"label": "Daily Yield"
},
{
"fieldname": "capacity_per_day",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Capacity"
},
{
"fieldname": "no_of_units_produced",
"fieldtype": "Int",
"label": "No of Units Produced"
},
{
"description": "In Days",
"fieldname": "purchase_time",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Purchase Time"
},
{
"description": "Per Day",
"fieldname": "shift_time_in_hours",
"fieldtype": "Int",
"label": "Shift Time (In Hours)"
},
{
"fieldname": "column_break_yilv",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_wuqi",
"fieldtype": "Section Break",
"label": "Manufacturing"
},
{
"fieldname": "column_break_cdqv",
"fieldtype": "Column Break"
},
{
"description": "Similar types of workstations where the same operations run in parallel.",
"fieldname": "no_of_workstations",
"fieldtype": "Int",
"label": "No of Workstations"
},
{
"fieldname": "item_details_tab",
"fieldtype": "Tab Break",
"label": "Item Details"
},
{
"fetch_from": "item_code.stock_uom",
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
"options": "UOM",
"read_only": 1
},
{
"fieldname": "purchase_lead_time_tab",
"fieldtype": "Tab Break",
"label": "Purchase Time"
},
{
"default": "1",
"fieldname": "no_of_days",
"fieldtype": "Int",
"label": "No of Days"
},
{
"fieldname": "manufacturing_time_tab",
"fieldtype": "Tab Break",
"label": "Manufacturing Time"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-08-31 13:12:38.458052",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Lead Time",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,34 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ItemLeadTime(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
bom_no: DF.Link | None
buffer_time: DF.Int
capacity_per_day: DF.Int
daily_yield: DF.Percent
item_code: DF.Link | None
item_name: DF.Data | None
manufacturing_time_in_mins: DF.Int
no_of_days: DF.Int
no_of_shift: DF.Int
no_of_units_produced: DF.Int
no_of_workstations: DF.Int
purchase_time: DF.Int
shift_time_in_hours: DF.Int
stock_uom: DF.Link | None
total_workstation_time: DF.Int
# end: auto-generated types
pass

View File

@@ -0,0 +1,20 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestItemLeadTime(IntegrationTestCase):
"""
Integration tests for ItemLeadTime.
Use this class for testing interactions between multiple components.
"""
pass

View File

@@ -23,7 +23,9 @@ dependencies = [
"pypng~=0.20220715.0",
# MT940 parser for bank statements
"mt-940>=4.26.0"
"mt-940>=4.26.0",
"pandas~=2.2.2",
"statsmodels~=0.14.5",
]
[build-system]