mirror of
https://github.com/frappe/erpnext.git
synced 2025-12-03 18:35:36 +00:00
feat: demand planning, MPS and MRP
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
"order_confirmation_no",
|
"order_confirmation_no",
|
||||||
"order_confirmation_date",
|
"order_confirmation_date",
|
||||||
"get_items_from_open_material_requests",
|
"get_items_from_open_material_requests",
|
||||||
|
"mps",
|
||||||
"column_break_7",
|
"column_break_7",
|
||||||
"transaction_date",
|
"transaction_date",
|
||||||
"schedule_date",
|
"schedule_date",
|
||||||
@@ -1315,6 +1316,13 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"is_virtual": 1,
|
"is_virtual": 1,
|
||||||
"label": "Last Scanned Warehouse"
|
"label": "Last Scanned Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "mps",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "MPS",
|
||||||
|
"options": "Master Production Schedule",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -1322,7 +1330,7 @@
|
|||||||
"idx": 105,
|
"idx": 105,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-07-31 17:19:40.816883",
|
"modified": "2025-08-28 11:00:56.635116",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order",
|
"name": "Purchase Order",
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ class PurchaseOrder(BuyingController):
|
|||||||
items: DF.Table[PurchaseOrderItem]
|
items: DF.Table[PurchaseOrderItem]
|
||||||
language: DF.Data | None
|
language: DF.Data | None
|
||||||
letter_head: DF.Link | None
|
letter_head: DF.Link | None
|
||||||
|
mps: DF.Link | None
|
||||||
named_place: DF.Data | None
|
named_place: DF.Data | None
|
||||||
naming_series: DF.Literal["PUR-ORD-.YYYY.-"]
|
naming_series: DF.Literal["PUR-ORD-.YYYY.-"]
|
||||||
net_total: DF.Currency
|
net_total: DF.Currency
|
||||||
|
|||||||
@@ -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();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"sales_order_date",
|
"sales_order_date",
|
||||||
"col_break1",
|
"col_break1",
|
||||||
"customer",
|
"customer",
|
||||||
"grand_total"
|
"grand_total",
|
||||||
|
"status"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -58,17 +59,25 @@
|
|||||||
"print_width": "120px",
|
"print_width": "120px",
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"width": "120px"
|
"width": "120px"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Status"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:20.746852",
|
"modified": "2025-08-21 15:16:13.828240",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Production Plan Sales Order",
|
"name": "Production Plan Sales Order",
|
||||||
|
"naming_rule": "Random",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": []
|
"states": []
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class ProductionPlanSalesOrder(Document):
|
|||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
sales_order: DF.Link
|
sales_order: DF.Link
|
||||||
sales_order_date: DF.Date | None
|
sales_order_date: DF.Date | None
|
||||||
|
status: DF.Data | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
252
erpnext/manufacturing/doctype/sales_forecast/sales_forecast.json
Normal file
252
erpnext/manufacturing/doctype/sales_forecast/sales_forecast.json
Normal 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": []
|
||||||
|
}
|
||||||
228
erpnext/manufacturing/doctype/sales_forecast/sales_forecast.py
Normal file
228
erpnext/manufacturing/doctype/sales_forecast/sales_forecast.py
Normal 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
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from frappe import _
|
||||||
|
|
||||||
|
|
||||||
|
def get_data():
|
||||||
|
return {
|
||||||
|
"fieldname": "demand_planning",
|
||||||
|
"transactions": [
|
||||||
|
{
|
||||||
|
"label": _("MPS"),
|
||||||
|
"items": ["Master Production Schedule"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -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"];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"item_name",
|
"item_name",
|
||||||
"image",
|
"image",
|
||||||
"bom_no",
|
"bom_no",
|
||||||
|
"mps",
|
||||||
"sales_order",
|
"sales_order",
|
||||||
"column_break1",
|
"column_break1",
|
||||||
"company",
|
"company",
|
||||||
@@ -601,6 +602,13 @@
|
|||||||
"label": "Disassembled Qty",
|
"label": "Disassembled Qty",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "mps",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "MPS",
|
||||||
|
"options": "Master Production Schedule",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -609,7 +617,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-06-21 00:55:45.916224",
|
"modified": "2025-08-28 11:01:48.719824",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Work Order",
|
"name": "Work Order",
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ class WorkOrder(Document):
|
|||||||
material_request: DF.Link | None
|
material_request: DF.Link | None
|
||||||
material_request_item: DF.Data | None
|
material_request_item: DF.Data | None
|
||||||
material_transferred_for_manufacturing: DF.Float
|
material_transferred_for_manufacturing: DF.Float
|
||||||
|
mps: DF.Link | None
|
||||||
naming_series: DF.Literal["MFG-WO-.YYYY.-"]
|
naming_series: DF.Literal["MFG-WO-.YYYY.-"]
|
||||||
operations: DF.Table[WorkOrderOperation]
|
operations: DF.Table[WorkOrderOperation]
|
||||||
planned_end_date: DF.Datetime | None
|
planned_end_date: DF.Datetime | None
|
||||||
|
|||||||
@@ -390,10 +390,18 @@ def get_time_logs(job_cards):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_default_holiday_list():
|
def get_default_holiday_list(company=None):
|
||||||
return frappe.get_cached_value(
|
if company:
|
||||||
"Company", frappe.defaults.get_user_default("Company"), "default_holiday_list"
|
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):
|
def check_if_within_operating_hours(workstation, operation, from_datetime, to_datetime):
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -212,6 +212,7 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
"Purchase Order",
|
"Purchase Order",
|
||||||
"Unreconcile Payment",
|
"Unreconcile Payment",
|
||||||
"Unreconcile Payment Entries",
|
"Unreconcile Payment Entries",
|
||||||
|
"Delivery Schedule Item",
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -545,6 +546,209 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
};
|
};
|
||||||
frappe.set_route("query-report", "Reserved Stock");
|
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", {
|
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"]);
|
frm.script_manager.copy_from_first_row("items", row, ["delivery_date"]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
delivery_date: function (frm, cdt, cdn) {
|
delivery_date: function (frm, cdt, cdn) {
|
||||||
if (!frm.doc.delivery_date) {
|
if (!frm.doc.delivery_date) {
|
||||||
erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "items", "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 {
|
erpnext.selling.SalesOrderController = class SalesOrderController extends erpnext.selling.SellingController {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from frappe.desk.notifications import clear_doctype_notifications
|
|||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.model.utils import get_fetch_values
|
from frappe.model.utils import get_fetch_values
|
||||||
from frappe.query_builder.functions import Sum
|
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 (
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||||
unlink_inter_company_doc,
|
unlink_inter_company_doc,
|
||||||
@@ -467,6 +467,7 @@ class SalesOrder(SellingController):
|
|||||||
if self.status == "Closed":
|
if self.status == "Closed":
|
||||||
frappe.throw(_("Closed order cannot be cancelled. Unclose to cancel."))
|
frappe.throw(_("Closed order cannot be cancelled. Unclose to cancel."))
|
||||||
|
|
||||||
|
self.delete_delivery_schedule_items()
|
||||||
self.check_nextdoc_docstatus()
|
self.check_nextdoc_docstatus()
|
||||||
self.update_reserved_qty()
|
self.update_reserved_qty()
|
||||||
self.update_project()
|
self.update_project()
|
||||||
@@ -791,6 +792,79 @@ class SalesOrder(SellingController):
|
|||||||
if not item.delivery_date:
|
if not item.delivery_date:
|
||||||
item.delivery_date = self.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:
|
def get_unreserved_qty(item: object, reserved_qty_details: dict) -> float:
|
||||||
"""Returns the unreserved quantity for the Sales Order Item."""
|
"""Returns the unreserved quantity for the Sales Order Item."""
|
||||||
|
|||||||
@@ -29,5 +29,6 @@ def get_data():
|
|||||||
{"label": _("Manufacturing"), "items": ["Work Order", "BOM", "Blanket Order"]},
|
{"label": _("Manufacturing"), "items": ["Work Order", "BOM", "Blanket Order"]},
|
||||||
{"label": _("Reference"), "items": ["Quotation", "Auto Repeat", "Stock Reservation Entry"]},
|
{"label": _("Reference"), "items": ["Quotation", "Auto Repeat", "Stock Reservation Entry"]},
|
||||||
{"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]},
|
{"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]},
|
||||||
|
{"label": _("Schedule"), "items": ["Delivery Schedule Item"]},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,8 @@
|
|||||||
"actual_qty",
|
"actual_qty",
|
||||||
"column_break_jpky",
|
"column_break_jpky",
|
||||||
"company_total_stock",
|
"company_total_stock",
|
||||||
|
"sales_order_schedule_section",
|
||||||
|
"add_schedule",
|
||||||
"manufacturing_section_section",
|
"manufacturing_section_section",
|
||||||
"bom_no",
|
"bom_no",
|
||||||
"planning_section",
|
"planning_section",
|
||||||
@@ -965,18 +967,29 @@
|
|||||||
"label": "Project",
|
"label": "Project",
|
||||||
"options": "Project",
|
"options": "Project",
|
||||||
"search_index": 1
|
"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,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-02-28 09:45:43.934947",
|
"modified": "2025-08-21 17:01:54.269105",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order Item",
|
"name": "Sales Order Item",
|
||||||
"naming_rule": "Random",
|
"naming_rule": "Random",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
"icon": "fa fa-compass",
|
"icon": "fa fa-compass",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:57.375141",
|
"modified": "2025-08-21 18:59:27.900209",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Setup",
|
"module": "Setup",
|
||||||
"name": "UOM",
|
"name": "UOM",
|
||||||
@@ -98,9 +98,28 @@
|
|||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Stock User"
|
"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,
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
|
|||||||
@@ -517,6 +517,16 @@ $.extend(erpnext.item, {
|
|||||||
},
|
},
|
||||||
__("Actions")
|
__("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) {
|
weight_to_validate: function (frm) {
|
||||||
|
|||||||
@@ -116,7 +116,15 @@
|
|||||||
"customer_code",
|
"customer_code",
|
||||||
"default_item_manufacturer",
|
"default_item_manufacturer",
|
||||||
"default_manufacturer_part_no",
|
"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": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -888,6 +896,46 @@
|
|||||||
"fieldname": "deferred_accounting_section",
|
"fieldname": "deferred_accounting_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Deferred Accounting"
|
"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",
|
"icon": "fa fa-tag",
|
||||||
@@ -895,7 +943,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2025-08-08 14:58:48.674193",
|
"modified": "2025-08-14 23:35:56.293048",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Item",
|
"name": "Item",
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class Item(Document):
|
|||||||
brand: DF.Link | None
|
brand: DF.Link | None
|
||||||
country_of_origin: DF.Link | None
|
country_of_origin: DF.Link | None
|
||||||
create_new_batch: DF.Check
|
create_new_batch: DF.Check
|
||||||
|
cumulative_time: DF.Int
|
||||||
customer: DF.Link | None
|
customer: DF.Link | None
|
||||||
customer_code: DF.SmallText | None
|
customer_code: DF.SmallText | None
|
||||||
customer_items: DF.Table[ItemCustomerDetail]
|
customer_items: DF.Table[ItemCustomerDetail]
|
||||||
@@ -119,6 +120,7 @@ class Item(Document):
|
|||||||
item_name: DF.Data | None
|
item_name: DF.Data | None
|
||||||
last_purchase_rate: DF.Float
|
last_purchase_rate: DF.Float
|
||||||
lead_time_days: DF.Int
|
lead_time_days: DF.Int
|
||||||
|
manufacturing_time: DF.Int
|
||||||
max_discount: DF.Float
|
max_discount: DF.Float
|
||||||
min_order_qty: DF.Float
|
min_order_qty: DF.Float
|
||||||
naming_series: DF.Literal["STO-ITEM-.YYYY.-"]
|
naming_series: DF.Literal["STO-ITEM-.YYYY.-"]
|
||||||
@@ -127,6 +129,9 @@ class Item(Document):
|
|||||||
opening_stock: DF.Float
|
opening_stock: DF.Float
|
||||||
over_billing_allowance: DF.Float
|
over_billing_allowance: DF.Float
|
||||||
over_delivery_receipt_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
|
purchase_uom: DF.Link | None
|
||||||
quality_inspection_template: DF.Link | None
|
quality_inspection_template: DF.Link | None
|
||||||
reorder_levels: DF.Table[ItemReorder]
|
reorder_levels: DF.Table[ItemReorder]
|
||||||
@@ -216,10 +221,16 @@ class Item(Document):
|
|||||||
self.validate_auto_reorder_enabled_in_stock_settings()
|
self.validate_auto_reorder_enabled_in_stock_settings()
|
||||||
self.cant_change()
|
self.cant_change()
|
||||||
self.validate_item_tax_net_rate_range()
|
self.validate_item_tax_net_rate_range()
|
||||||
|
self.set_cumulative_time()
|
||||||
|
|
||||||
if not self.is_new():
|
if not self.is_new():
|
||||||
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
|
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):
|
def on_update(self):
|
||||||
self.update_variants()
|
self.update_variants()
|
||||||
self.update_item_price()
|
self.update_item_price()
|
||||||
|
|||||||
@@ -32,5 +32,6 @@ def get_data():
|
|||||||
{"label": _("Manufacture"), "items": ["Production Plan", "Work Order", "Item Manufacturer"]},
|
{"label": _("Manufacture"), "items": ["Production Plan", "Work Order", "Item Manufacturer"]},
|
||||||
{"label": _("Traceability"), "items": ["Serial No", "Batch"]},
|
{"label": _("Traceability"), "items": ["Serial No", "Batch"]},
|
||||||
{"label": _("Stock Movement"), "items": ["Stock Entry", "Stock Reconciliation"]},
|
{"label": _("Stock Movement"), "items": ["Stock Entry", "Stock Reconciliation"]},
|
||||||
|
{"label": _("Lead Time"), "items": ["Item Lead Time"]},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
0
erpnext/stock/doctype/item_lead_time/__init__.py
Normal file
0
erpnext/stock/doctype/item_lead_time/__init__.py
Normal file
65
erpnext/stock/doctype/item_lead_time/item_lead_time.js
Normal file
65
erpnext/stock/doctype/item_lead_time/item_lead_time.js
Normal 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));
|
||||||
|
},
|
||||||
|
});
|
||||||
216
erpnext/stock/doctype/item_lead_time/item_lead_time.json
Normal file
216
erpnext/stock/doctype/item_lead_time/item_lead_time.json
Normal 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": []
|
||||||
|
}
|
||||||
34
erpnext/stock/doctype/item_lead_time/item_lead_time.py
Normal file
34
erpnext/stock/doctype/item_lead_time/item_lead_time.py
Normal 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
|
||||||
20
erpnext/stock/doctype/item_lead_time/test_item_lead_time.py
Normal file
20
erpnext/stock/doctype/item_lead_time/test_item_lead_time.py
Normal 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
|
||||||
@@ -23,7 +23,9 @@ dependencies = [
|
|||||||
"pypng~=0.20220715.0",
|
"pypng~=0.20220715.0",
|
||||||
|
|
||||||
# MT940 parser for bank statements
|
# 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]
|
[build-system]
|
||||||
|
|||||||
Reference in New Issue
Block a user