diff --git a/erpnext/config/manufacturing.py b/erpnext/config/manufacturing.py index 2c18eeb83a1..012f1cad0ad 100644 --- a/erpnext/config/manufacturing.py +++ b/erpnext/config/manufacturing.py @@ -120,13 +120,7 @@ def get_data(): { "type": "report", "is_query_report": True, - "name": "Open Work Orders", - "doctype": "Work Order" - }, - { - "type": "report", - "is_query_report": True, - "name": "Work Orders in Progress", + "name": "Work Order Summary", "doctype": "Work Order" }, { @@ -135,12 +129,6 @@ def get_data(): "name": "Issued Items Against Work Order", "doctype": "Work Order" }, - { - "type": "report", - "is_query_report": True, - "name": "Completed Work Orders", - "doctype": "Work Order" - }, { "type": "report", "is_query_report": True, diff --git a/erpnext/manufacturing/dashboard_fixtures.py b/erpnext/manufacturing/dashboard_fixtures.py new file mode 100644 index 00000000000..587a0322415 --- /dev/null +++ b/erpnext/manufacturing/dashboard_fixtures.py @@ -0,0 +1,240 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe, erpnext, json +from frappe import _ +from frappe.utils import nowdate, get_date_str +from erpnext.accounts.utils import get_fiscal_year + +def get_data(): + return frappe._dict({ + "dashboards": get_dashboards(), + "charts": get_charts(), + "number_cards": get_number_cards(), + }) + +def get_dashboards(): + return [{ + "name": "Manufacturing Dashboard", + "dashboard_name": "Manufacturing Dashboard", + "charts": [ + { "chart": "Produced Quantity", "width": "Half" }, + { "chart": "Completed Operation", "width": "Half" }, + { "chart": "Work Order Analysis", "width": "Half" }, + { "chart": "Quality Inspection Analysis", "width": "Half" }, + { "chart": "Pending Work Order", "width": "Half" }, + { "chart": "Last Month Downtime Analysis", "width": "Half" }, + { "chart": "Work Order Qty Analysis", "width": "Full" }, + { "chart": "Job Card Analysis", "width": "Full" } + ], + "cards": [ + { "card": "Total Work Order" }, + { "card": "Completed Work Order" }, + { "card": "Ongoing Job Card" }, + { "card": "Total Quality Inspection"} + ] + }] + +def get_charts(): + company = erpnext.get_default_company() + + if not company: + company = frappe.db.get_value("Company", {"is_group": 0}, "name") + + return [{ + "doctype": "Dashboard Chart", + "based_on": "modified", + "time_interval": "Yearly", + "chart_type": "Sum", + "chart_name": _("Produced Quantity"), + "name": "Produced Quantity", + "document_type": "Work Order", + "filters_json": json.dumps([['Work Order', 'docstatus', '=', 1, False]]), + "group_by_type": "Count", + "time_interval": "Monthly", + "timespan": "Last Year", + "owner": "Administrator", + "type": "Line", + "value_based_on": "produced_qty", + "is_public": 1, + "timeseries": 1 + }, { + "doctype": "Dashboard Chart", + "based_on": "creation", + "time_interval": "Yearly", + "chart_type": "Sum", + "chart_name": _("Completed Operation"), + "name": "Completed Operation", + "document_type": "Work Order Operation", + "filters_json": json.dumps([['Work Order Operation', 'docstatus', '=', 1, False]]), + "group_by_type": "Count", + "time_interval": "Quarterly", + "timespan": "Last Year", + "owner": "Administrator", + "type": "Line", + "value_based_on": "completed_qty", + "is_public": 1, + "timeseries": 1 + }, { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Work Order Analysis"), + "name": "Work Order Analysis", + "timespan": "Last Year", + "report_name": "Work Order Summary", + "owner": "Administrator", + "filters_json": json.dumps({"company": company, "charts_based_on": "Status"}), + "type": "Donut", + "is_public": 1, + "is_custom": 1, + "custom_options": json.dumps({ + "axisOptions": { + "shortenYAxisNumbers": 1 + }, + "height": 300 + }), + }, { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Quality Inspection Analysis"), + "name": "Quality Inspection Analysis", + "timespan": "Last Year", + "report_name": "Quality Inspection Summary", + "owner": "Administrator", + "filters_json": json.dumps({}), + "type": "Donut", + "is_public": 1, + "is_custom": 1, + "custom_options": json.dumps({ + "axisOptions": { + "shortenYAxisNumbers": 1 + }, + "height": 300 + }), + }, { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Pending Work Order"), + "name": "Pending Work Order", + "timespan": "Last Year", + "report_name": "Work Order Summary", + "filters_json": json.dumps({"company": company, "charts_based_on": "Age"}), + "owner": "Administrator", + "type": "Donut", + "is_public": 1, + "is_custom": 1, + "custom_options": json.dumps({ + "axisOptions": { + "shortenYAxisNumbers": 1 + }, + "height": 300 + }), + }, { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Last Month Downtime Analysis"), + "name": "Last Month Downtime Analysis", + "timespan": "Last Year", + "filters_json": json.dumps({}), + "report_name": "Downtime Analysis", + "owner": "Administrator", + "is_public": 1, + "is_custom": 1, + "type": "Bar" + }, { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Work Order Qty Analysis"), + "name": "Work Order Qty Analysis", + "timespan": "Last Year", + "report_name": "Work Order Summary", + "filters_json": json.dumps({"company": company, "charts_based_on": "Quantity"}), + "owner": "Administrator", + "type": "Bar", + "is_public": 1, + "is_custom": 1, + "custom_options": json.dumps({ + "barOptions": { "stacked": 1 } + }), + }, { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Job Card Analysis"), + "name": "Job Card Analysis", + "timespan": "Last Year", + "report_name": "Job Card Summary", + "owner": "Administrator", + "is_public": 1, + "is_custom": 1, + "filters_json": json.dumps({"company": company, "docstatus": 1, "range":"Monthly"}), + "custom_options": json.dumps({ + "barOptions": { "stacked": 1 } + }), + "type": "Bar" + }] + +def get_number_cards(): + fiscal_year = get_fiscal_year(date=nowdate()) + year_start_date = get_date_str(fiscal_year[1]) + year_end_date = get_date_str(fiscal_year[2]) + + return [{ + "doctype": "Number Card", + "document_type": "Work Order", + "name": "Total Work Order", + "filters_json": json.dumps([ + ['Work Order', 'docstatus', '=', 1], + ['Work Order', 'creation', 'between', [year_start_date, year_end_date]] + ]), + "function": "Count", + "is_public": 1, + "label": _("Total Work Order"), + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }, + { + "doctype": "Number Card", + "document_type": "Work Order", + "name": "Completed Work Order", + "filters_json": json.dumps([ + ['Work Order', 'status', '=', 'Completed'], + ['Work Order', 'docstatus', '=', 1], + ['Work Order', 'creation', 'between', [year_start_date, year_end_date]] + ]), + "function": "Count", + "is_public": 1, + "label": _("Completed Work Order"), + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }, + { + "doctype": "Number Card", + "document_type": "Job Card", + "name": "Ongoing Job Card", + "filters_json": json.dumps([ + ['Job Card', 'status','!=','Completed'], + ['Job Card', 'docstatus', '=', 1] + ]), + "function": "Count", + "is_public": 1, + "label": _("Ongoing Job Card"), + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }, + { + "doctype": "Number Card", + "document_type": "Quality Inspection", + "name": "Total Quality Inspection", + "filters_json": json.dumps([['Quality Inspection', 'docstatus', '=', 1]]), + "function": "Count", + "is_public": 1, + "label": _("Total Quality Inspection"), + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }] \ No newline at end of file diff --git a/erpnext/manufacturing/desk_page/manufacturing/manufacturing.json b/erpnext/manufacturing/desk_page/manufacturing/manufacturing.json index 18604e283ab..c181d5d5361 100644 --- a/erpnext/manufacturing/desk_page/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/desk_page/manufacturing/manufacturing.json @@ -13,7 +13,7 @@ { "hidden": 0, "label": "Reports", - "links": "[\n {\n \"dependencies\": [\n \"Work Order\"\n ],\n \"doctype\": \"Work Order\",\n \"is_query_report\": true,\n \"label\": \"Open Work Orders\",\n \"name\": \"Open Work Orders\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Work Order\"\n ],\n \"doctype\": \"Work Order\",\n \"is_query_report\": true,\n \"label\": \"Work Orders in Progress\",\n \"name\": \"Work Orders in Progress\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Work Order\"\n ],\n \"doctype\": \"Work Order\",\n \"is_query_report\": true,\n \"label\": \"Issued Items Against Work Order\",\n \"name\": \"Issued Items Against Work Order\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Work Order\"\n ],\n \"doctype\": \"Work Order\",\n \"is_query_report\": true,\n \"label\": \"Completed Work Orders\",\n \"name\": \"Completed Work Orders\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Work Order\"\n ],\n \"doctype\": \"Work Order\",\n \"is_query_report\": true,\n \"label\": \"Production Analytics\",\n \"name\": \"Production Analytics\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"BOM\"\n ],\n \"doctype\": \"BOM\",\n \"is_query_report\": true,\n \"label\": \"BOM Search\",\n \"name\": \"BOM Search\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"BOM\"\n ],\n \"doctype\": \"BOM\",\n \"is_query_report\": true,\n \"label\": \"BOM Stock Report\",\n \"name\": \"BOM Stock Report\",\n \"type\": \"report\"\n }\n]" + "links": "[{\n\t\"dependencies\": [\"Work Order\"],\n\t\"name\": \"Work Order Summary\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Work Order\",\n\t\"label\": \"Work Order Summary\"\n}, {\n\t\"dependencies\": [\"Work Order\"],\n\t\"name\": \"Production Analytics\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Work Order\",\n\t\"label\": \"Production Analytics\"\n}, {\n\t\"dependencies\": [\"Quality Inspection\"],\n\t\"name\": \"Quality Inspection Summary\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Quality Inspection\",\n\t\"label\": \"Quality Inspection Summary\"\n}, {\n\t\"dependencies\": [\"Downtime Entry\"],\n\t\"name\": \"Downtime Analysis\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Downtime Entry\",\n\t\"label\": \"Downtime Analysis\"\n}, {\n\t\"dependencies\": [\"Job Card\"],\n\t\"name\": \"Job Card Summary\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Job Card\",\n\t\"label\": \"Job Card Summary\"\n}, {\n\t\"dependencies\": [\"BOM\"],\n\t\"name\": \"BOM Search\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"BOM\",\n\t\"label\": \"BOM Search\"\n}, {\n\t\"dependencies\": [\"BOM\"],\n\t\"name\": \"BOM Stock Report\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"BOM\",\n\t\"label\": \"BOM Stock Report\"\n}]" }, { "hidden": 0, @@ -32,23 +32,74 @@ } ], "category": "Domains", - "charts": [], + "charts": [ + { + "chart_name": "Produced Quantity" + } + ], "creation": "2020-03-02 17:11:37.032604", "developer_mode_only": 0, "disable_user_customization": 0, "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Manufacturing", - "modified": "2020-04-01 11:28:50.979358", + "modified": "2020-05-19 12:06:25.047481", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", + "onboarding": "Manufacturing", "owner": "Administrator", "pin_to_bottom": 0, "pin_to_top": 0, "restrict_to_domain": "Manufacturing", - "shortcuts": [] + "shortcuts": [ + { + "format": "{} Active", + "label": "Item", + "link_to": "Item", + "restrict_to_domain": "Manufacturing", + "stats_filter": "{\n \"disabled\": 0\n}", + "type": "DocType" + }, + { + "format": "{} Active", + "label": "BOM", + "link_to": "BOM", + "restrict_to_domain": "Manufacturing", + "stats_filter": "{\n \"is_active\": 1\n}", + "type": "DocType" + }, + { + "format": "{} Open", + "label": "Work Order", + "link_to": "Work Order", + "restrict_to_domain": "Manufacturing", + "stats_filter": "{ \"status\": \n (\n \"in\", [\"Draft\", \"Not Started\", \"In Process\"]\n )\n}", + "type": "DocType" + }, + { + "format": "{} Open", + "label": "Production Plan", + "link_to": "Production Plan", + "restrict_to_domain": "Manufacturing", + "stats_filter": "{ \"status\": \n (\n \"!=\", \"Completed\"\n )\n}", + "type": "DocType" + }, + { + "label": "Work Order Summary", + "link_to": "Work Order Summary", + "restrict_to_domain": "Manufacturing", + "type": "Report" + }, + { + "label": "Manufacturing Dashboard", + "link_to": "Manufacturing Dashboard", + "restrict_to_domain": "Manufacturing", + "type": "Dashboard" + } + ] } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/downtime_entry/__init__.py b/erpnext/manufacturing/doctype/downtime_entry/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.js b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.js new file mode 100644 index 00000000000..3b7f5ba8d7f --- /dev/null +++ b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Downtime Entry', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.json b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.json new file mode 100644 index 00000000000..6ec088ad9e7 --- /dev/null +++ b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.json @@ -0,0 +1,120 @@ +{ + "actions": [], + "allow_import": 1, + "creation": "2020-04-18 04:50:46.187638", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "workstation", + "operator", + "column_break_4", + "from_time", + "to_time", + "downtime", + "downtime_reason_section", + "reason" + ], + "fields": [ + { + "fieldname": "workstation", + "fieldtype": "Link", + "label": "Workstation / Machine", + "options": "Workstation", + "reqd": 1 + }, + { + "fieldname": "from_time", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "From Time", + "reqd": 1 + }, + { + "fieldname": "to_time", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "To Time", + "reqd": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "reason", + "fieldtype": "Text", + "label": "Reason", + "reqd": 1 + }, + { + "fieldname": "operator", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Operator", + "options": "Employee", + "reqd": 1 + }, + { + "fieldname": "downtime_reason_section", + "fieldtype": "Section Break", + "label": "Downtime Reason" + }, + { + "description": "In Mins", + "fieldname": "downtime", + "fieldtype": "Float", + "label": "Downtime", + "read_only": 1 + } + ], + "links": [], + "modified": "2020-04-20 17:34:51.299607", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Downtime Entry", + "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 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "workstation", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.py b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.py new file mode 100644 index 00000000000..56ec4356af4 --- /dev/null +++ b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import time_diff_in_hours +from frappe.model.document import Document + +class DowntimeEntry(Document): + def validate(self): + if self.from_time and self.to_time: + self.downtime = time_diff_in_hours(self.to_time, self.from_time) * 60 diff --git a/erpnext/manufacturing/doctype/downtime_entry/test_downtime_entry.py b/erpnext/manufacturing/doctype/downtime_entry/test_downtime_entry.py new file mode 100644 index 00000000000..8b2a8d36c12 --- /dev/null +++ b/erpnext/manufacturing/doctype/downtime_entry/test_downtime_entry.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestDowntimeEntry(unittest.TestCase): + pass diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 7661fffa864..fba670c1c15 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2018-07-09 17:23:29.518745", "doctype": "DocType", @@ -264,8 +265,10 @@ { "fetch_from": "work_order.production_item", "fieldname": "production_item", - "fieldtype": "Read Only", - "label": "Production Item" + "fieldtype": "Link", + "label": "Production Item", + "options": "Item", + "read_only": 1 }, { "fieldname": "barcode", @@ -274,7 +277,8 @@ "read_only": 1 }, { - "fetch_from": "work_order.item_name", + "fetch_from": "production_item.item_name", + "fetch_if_empty": 1, "fieldname": "item_name", "fieldtype": "Read Only", "label": "Item Name" @@ -290,7 +294,8 @@ } ], "is_submittable": 1, - "modified": "2020-03-27 13:36:35.417502", + "links": [], + "modified": "2020-04-20 15:14:00.273441", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 00a67a03d67..585a09db2bf 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -38,11 +38,12 @@ "required_items", "time", "planned_start_date", - "actual_start_date", - "column_break_13", "planned_end_date", - "actual_end_date", "expected_delivery_date", + "column_break_13", + "actual_start_date", + "actual_end_date", + "lead_time", "operations_section", "transfer_material_against", "operations", @@ -108,6 +109,8 @@ }, { "depends_on": "eval:doc.production_item", + "fetch_from": "production_item.item_name", + "fetch_if_empty": 1, "fieldname": "item_name", "fieldtype": "Data", "label": "Item Name", @@ -281,27 +284,30 @@ "reqd": 1 }, { + "allow_on_submit": 1, "fieldname": "actual_start_date", "fieldtype": "Datetime", "label": "Actual Start Date", - "read_only": 1 + "read_only_depends_on": "eval:doc.operations && doc.operations.length > 0" }, { "fieldname": "column_break_13", "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "fieldname": "planned_end_date", "fieldtype": "Datetime", "label": "Planned End Date", "no_copy": 1, - "read_only": 1 + "read_only_depends_on": "eval:doc.operations && doc.operations.length > 0" }, { + "allow_on_submit": 1, "fieldname": "actual_end_date", "fieldtype": "Datetime", "label": "Actual End Date", - "read_only": 1 + "read_only_depends_on": "eval:doc.operations && doc.operations.length > 0" }, { "allow_on_submit": 1, @@ -476,6 +482,13 @@ "fieldtype": "Link", "label": "Source Warehouse", "options": "Warehouse" + }, + { + "description": "In Mins", + "fieldname": "lead_time", + "fieldtype": "Float", + "label": "Lead Time", + "read_only": 1 } ], "icon": "fa fa-cogs", @@ -483,7 +496,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2020-04-24 19:32:43.323054", + "modified": "2020-05-05 19:32:43.323054", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 8301f30d837..c2789559b07 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -6,7 +6,7 @@ import frappe import json import math from frappe import _ -from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate, get_link_to_form +from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate, get_link_to_form, time_diff_in_hours from frappe.model.document import Document from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_bom_items_as_dict from dateutil.relativedelta import relativedelta @@ -279,7 +279,7 @@ class WorkOrder(Document): if enable_capacity_planning and job_card_doc: row.planned_start_time = job_card_doc.time_logs[-1].from_time row.planned_end_time = job_card_doc.time_logs[-1].to_time - print(row.planned_start_time, original_start_time, plan_days) + if date_diff(row.planned_start_time, original_start_time) > plan_days: frappe.message_log.pop() frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.") @@ -437,8 +437,6 @@ class WorkOrder(Document): frappe.throw(_("Completed Qty can not be greater than 'Qty to Manufacture'")) def set_actual_dates(self): - self.actual_start_date = None - self.actual_end_date = None if self.get("operations"): actual_start_dates = [d.actual_start_time for d in self.get("operations") if d.actual_start_time] if actual_start_dates: @@ -447,6 +445,27 @@ class WorkOrder(Document): actual_end_dates = [d.actual_end_time for d in self.get("operations") if d.actual_end_time] if actual_end_dates: self.actual_end_date = max(actual_end_dates) + else: + data = frappe.get_all("Stock Entry", + fields = ["timestamp(posting_date, posting_time) as posting_datetime"], + filters = { + "work_order": self.name, + "purpose": ("in", ["Material Transfer for Manufacture", "Manufacture"]) + } + ) + + if data and len(data): + dates = [d.posting_datetime for d in data] + self.actual_start_date = min(dates) + + if self.status == "Completed": + self.actual_end_date = max(dates) + + self.set_lead_time() + + def set_lead_time(self): + if self.actual_start_date and self.actual_end_date: + self.lead_time = flt(time_diff_in_hours(self.actual_end_date, self.actual_start_date) * 60) def delete_job_card(self): for d in frappe.get_all("Job Card", ["name"], {"work_order": self.name}): diff --git a/erpnext/manufacturing/module_onboarding/manufacturing/manufacturing.json b/erpnext/manufacturing/module_onboarding/manufacturing/manufacturing.json new file mode 100644 index 00000000000..4092e9d87cb --- /dev/null +++ b/erpnext/manufacturing/module_onboarding/manufacturing/manufacturing.json @@ -0,0 +1,54 @@ +{ + "allow_roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Manufacturing Manager" + }, + { + "role": "Item Manager" + }, + { + "role": "Stock User" + } + ], + "creation": "2020-05-05 16:37:08.238935", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/manufacturing", + "idx": 0, + "is_complete": 0, + "modified": "2020-05-14 19:12:17.289867", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Manufacturing", + "owner": "Administrator", + "steps": [ + { + "step": "Warehouse" + }, + { + "step": "Workstation" + }, + { + "step": "Operation" + }, + { + "step": "Create Product" + }, + { + "step": "Create BOM" + }, + { + "step": "Work Order" + }, + { + "step": "Introduction to Manufacturing" + } + ], + "subtitle": "Products, Raw Materials, BOM, Work Order and more.", + "success_message": "Manufacturing module is all setup!", + "title": "Let's Setup Manufacturing Module", + "user_can_dismiss": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding/manufacturing/manufacturing.json b/erpnext/manufacturing/onboarding/manufacturing/manufacturing.json new file mode 100644 index 00000000000..50584e19cac --- /dev/null +++ b/erpnext/manufacturing/onboarding/manufacturing/manufacturing.json @@ -0,0 +1,54 @@ +{ + "allow_roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Manufacturing Manager" + }, + { + "role": "Item Manager" + }, + { + "role": "Stock User" + } + ], + "creation": "2020-05-05 16:37:08.238935", + "docstatus": 0, + "doctype": "Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/manufacturing", + "idx": 0, + "is_complete": 0, + "modified": "2020-05-12 16:22:07.050224", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Manufacturing", + "owner": "Administrator", + "steps": [ + { + "step": "Introduction to Manufacturing" + }, + { + "step": "Warehouse" + }, + { + "step": "Workstation" + }, + { + "step": "Operation" + }, + { + "step": "Create Product" + }, + { + "step": "Create BOM" + }, + { + "step": "Work Order" + } + ], + "subtitle": "Products, Raw Materials, BOM, Work Order and more.", + "success_message": "Manufacturing module is all setup!", + "title": "Let's Setup Manufacturing Module", + "user_can_dismiss": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/create_bom/create_bom.json b/erpnext/manufacturing/onboarding_step/create_bom/create_bom.json new file mode 100644 index 00000000000..866b871f2b7 --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/create_bom/create_bom.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-05 16:41:20.239696", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 19:11:57.153679", + "modified_by": "Administrator", + "name": "Create BOM", + "owner": "Administrator", + "reference_document": "BOM", + "show_full_form": 1, + "title": "Create BOM (Bill of Material)", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/create_product/create_product.json b/erpnext/manufacturing/onboarding_step/create_product/create_product.json new file mode 100644 index 00000000000..dd2fcb4ce7f --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/create_product/create_product.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-05 16:42:31.476275", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 19:10:39.974370", + "modified_by": "Administrator", + "name": "Create Product", + "owner": "Administrator", + "reference_document": "Item", + "show_full_form": 0, + "title": "Create Product (Raw Materials / Finished Good)", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/introduction_to_manufacturing/introduction_to_manufacturing.json b/erpnext/manufacturing/onboarding_step/introduction_to_manufacturing/introduction_to_manufacturing.json new file mode 100644 index 00000000000..eb7ab3a175c --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/introduction_to_manufacturing/introduction_to_manufacturing.json @@ -0,0 +1,20 @@ +{ + "action": "Update Settings", + "creation": "2020-05-05 16:40:23.676406", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 19:11:57.152883", + "modified_by": "Administrator", + "name": "Introduction to Manufacturing", + "owner": "Administrator", + "reference_document": "Manufacturing Settings", + "show_full_form": 0, + "title": "Manufacturing Settings", + "validate_action": 1, + "video_url": "https://www.youtube.com/watch?v=UVGfzwOOZC4" +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/operation/operation.json b/erpnext/manufacturing/onboarding_step/operation/operation.json new file mode 100644 index 00000000000..86e62e74111 --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/operation/operation.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-12 16:15:31.706756", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 19:10:27.258157", + "modified_by": "Administrator", + "name": "Operation", + "owner": "Administrator", + "reference_document": "Operation", + "show_full_form": 0, + "title": "Create Operation", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/warehouse/warehouse.json b/erpnext/manufacturing/onboarding_step/warehouse/warehouse.json new file mode 100644 index 00000000000..f187f57414d --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/warehouse/warehouse.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-12 16:13:34.014554", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 19:10:07.439037", + "modified_by": "Administrator", + "name": "Warehouse", + "owner": "Administrator", + "reference_document": "Warehouse", + "show_full_form": 0, + "title": "Create Warehouse", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/work_order/work_order.json b/erpnext/manufacturing/onboarding_step/work_order/work_order.json new file mode 100644 index 00000000000..e757c23d122 --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/work_order/work_order.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-12 16:15:56.084682", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 19:11:57.145924", + "modified_by": "Administrator", + "name": "Work Order", + "owner": "Administrator", + "reference_document": "Work Order", + "show_full_form": 1, + "title": "Create Work Order", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/workstation/workstation.json b/erpnext/manufacturing/onboarding_step/workstation/workstation.json new file mode 100644 index 00000000000..b786443cbc7 --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/workstation/workstation.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-12 16:14:14.930214", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 19:10:20.930139", + "modified_by": "Administrator", + "name": "Workstation", + "owner": "Administrator", + "reference_document": "Workstation", + "show_full_form": 0, + "title": "Create Workstation / Machine", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/downtime_analysis/__init__.py b/erpnext/manufacturing/report/downtime_analysis/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js new file mode 100644 index 00000000000..e20342792f4 --- /dev/null +++ b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js @@ -0,0 +1,28 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Downtime Analysis"] = { + "filters": [ + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1 + }, + { + label: __("To Date"), + fieldname:"to_date", + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + }, + { + label: __("Machine"), + fieldname: "workstation", + fieldtype: "Link", + options: "Workstation" + } + ] +}; diff --git a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.json b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.json new file mode 100644 index 00000000000..5edc7781a2e --- /dev/null +++ b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.json @@ -0,0 +1,31 @@ +{ + "add_total_row": 1, + "creation": "2020-04-20 18:26:04.345289", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "Gadgets International", + "modified": "2020-04-20 18:26:04.345289", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Downtime Analysis", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Downtime Entry", + "report_name": "Downtime Analysis", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Manufacturing User" + }, + { + "role": "Manufacturing Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py new file mode 100644 index 00000000000..dfc6b02b84f --- /dev/null +++ b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py @@ -0,0 +1,95 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import flt +from frappe import _ + +def execute(filters=None): + columns, data = [], [] + data = get_data(filters) + columns = get_columns(filters) + chart_data = get_chart_data(data, filters) + return columns, data, None, chart_data + +def get_data(filters): + query_filters = {} + + fields = ["workstation", "operator", "from_time", "to_time", "downtime", "reason"] + + query_filters["from_time"] = (">=", filters.get("from_date")) + query_filters["to_time"] = ("<=", filters.get("to_date")) + + if filters.get("workstation"): + query_filters["workstation"] = filters.get("workstation") + + return frappe.get_all("Downtime Entry", fields= fields, filters=query_filters) + +def get_chart_data(data, columns): + labels = sorted(list(set([d.workstation for d in data]))) + + workstation_wise_data = {} + for d in data: + if d.workstation not in workstation_wise_data: + workstation_wise_data[d.workstation] = 0 + + workstation_wise_data[d.workstation] += flt(d.downtime, 2) + + datasets = [] + for label in labels: + datasets.append(workstation_wise_data.get(label, 0)) + + chart = { + "data": { + "labels": labels, + "datasets": [ + {"name": "Dataset 1", "values": datasets} + ] + }, + "type": "bar" + } + + return chart + +def get_columns(filters): + return [ + { + "label": _("Machine"), + "fieldname": "workstation", + "fieldtype": "Link", + "options": "Workstation", + "width": 100 + }, + { + "label": _("Operator"), + "fieldname": "operator", + "fieldtype": "Link", + "options": "Employee", + "width": 130 + }, + { + "label": _("From Time"), + "fieldname": "from_time", + "fieldtype": "Datetime", + "width": 160 + }, + { + "label": _("To Time"), + "fieldname": "to_time", + "fieldtype": "Datetime", + "width": 160 + }, + { + "label": _("Downtime (In Mins)"), + "fieldname": "downtime", + "fieldtype": "Float", + "width": 150 + }, + { + "label": _("Reason"), + "fieldname": "reason", + "fieldtype": "Text", + "width": 180 + } + ] \ No newline at end of file diff --git a/erpnext/manufacturing/report/job_card_summary/__init__.py b/erpnext/manufacturing/report/job_card_summary/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js new file mode 100644 index 00000000000..b7e307183f8 --- /dev/null +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js @@ -0,0 +1,52 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Job Card Summary"] = { + "filters": [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -12), + reqd: 1 + }, + { + label: __("To Date"), + fieldname:"to_date", + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + }, + { + label: __("Status"), + fieldname: "status", + fieldtype: "Select", + options: ["", "Open", "Work In Progress", "Completed", "On Hold"] + }, + { + label: __("Sales Orders"), + fieldname: "sales_order", + fieldtype: "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Sales Order', txt); + } + }, + { + label: __("Production Item"), + fieldname: "production_item", + fieldtype: "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Item', txt); + } + } + ] +}; diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.json b/erpnext/manufacturing/report/job_card_summary/job_card_summary.json new file mode 100644 index 00000000000..9f08fc34cb8 --- /dev/null +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.json @@ -0,0 +1,34 @@ +{ + "add_total_row": 0, + "creation": "2020-04-20 12:00:21.436619", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "Gadgets International", + "modified": "2020-04-20 12:00:21.436619", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Job Card Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Job Card", + "report_name": "Job Card Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Stock User" + }, + { + "role": "Manufacturing Manager" + }, + { + "role": "Stock Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py new file mode 100644 index 00000000000..ae1e4f30469 --- /dev/null +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py @@ -0,0 +1,186 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import getdate, flt +from erpnext.stock.report.stock_analytics.stock_analytics import (get_period_date_ranges, get_period) + +def execute(filters=None): + columns, data = [], [] + data = get_data(filters) + columns = get_columns(filters) + chart_data = get_chart_data(data, filters) + return columns, data, None, chart_data + +def get_data(filters): + query_filters = {"docstatus": ("<", 2)} + + fields = ["name", "status", "work_order", "production_item", "item_name", + "total_completed_qty", "workstation", "operation", "employee_name", "total_time_in_mins"] + + for field in ["work_order", "workstation", "operation", "company"]: + if filters.get(field): + query_filters[field] = ("in", filters.get(field)) + + data = frappe.get_all("Job Card", + fields= fields, filters=query_filters) + + if not data: return [] + + job_cards = [d.name for d in data] + job_card_time_details = {} + for job_card_data in frappe.get_all("Job Card Time Log", + fields=["min(from_time) as from_time", "max(to_time) as to_time", "parent"], + filters={"docstatus": ("<", 2), "parent": ("in", job_cards)}, group_by="parent"): + job_card_time_details[job_card_data.parent] = job_card_data + + for d in data: + if d.status == "Material Transferred": + d.status = "Open" + + if job_card_time_details.get(d.name): + d.from_time = job_card_time_details.get(d.name).from_time + d.to_time = job_card_time_details.get(d.name).to_time + + return data + +def get_chart_data(job_card_details, filters): + labels, periodic_data = prepare_chart_data(job_card_details, filters) + + pending, completed = [], [] + datasets = [] + + for d in labels: + pending.append(periodic_data.get("Pending").get(d)) + completed.append(periodic_data.get("Completed").get(d)) + + datasets.append({"name": "Pending", "values": pending}) + datasets.append({"name": "Completed", "values": completed}) + + chart = { + "data": { + 'labels': labels, + 'datasets': datasets + }, + "type": "bar" + } + + return chart + +def prepare_chart_data(job_card_details, filters): + labels = [] + + periodic_data = { + "Pending": {}, + "Completed": {} + } + + filters.range = "Monthly" + + ranges = get_period_date_ranges(filters) + for from_date, end_date in ranges: + period = get_period(end_date, filters) + if period not in labels: + labels.append(period) + + for d in job_card_details: + if getdate(d.from_time) >= from_date and getdate(d.to_time) <= end_date: + status = "Completed" if d.status == "Completed" else "Pending" + + if periodic_data.get(status) and periodic_data.get(status).get(period): + periodic_data[status][period] += 1 + else: + periodic_data[status][period] = 1 + + return labels, periodic_data + +def get_columns(filters): + columns = [ + { + "label": _("Id"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Job Card", + "width": 100 + }, + ] + + if not filters.get("status"): + columns.append( + { + "label": _("Status"), + "fieldname": "status", + "width": 100 + }, + ) + + columns.extend([ + { + "label": _("Work Order"), + "fieldname": "work_order", + "fieldtype": "Link", + "options": "Work Order", + "width": 100 + }, + { + "label": _("Production Item"), + "fieldname": "production_item", + "fieldtype": "Link", + "options": "Item", + "width": 110 + }, + { + "label": _("Item Name"), + "fieldname": "item_name", + "fieldtype": "Data", + "width": 100 + }, + { + "label": _("Workstation"), + "fieldname": "workstation", + "fieldtype": "Link", + "options": "Workstation", + "width": 110 + }, + { + "label": _("Operation"), + "fieldname": "operation", + "fieldtype": "Link", + "options": "Operation", + "width": 110 + }, + { + "label": _("Employee Name"), + "fieldname": "employee_name", + "fieldtype": "Data", + "width": 110 + }, + { + "label": _("Total Completed Qty"), + "fieldname": "total_completed_qty", + "fieldtype": "Float", + "width": 120 + }, + { + "label": _("From Time"), + "fieldname": "from_time", + "fieldtype": "Datetime", + "width": 120 + }, + { + "label": _("To Time"), + "fieldname": "to_time", + "fieldtype": "Datetime", + "width": 120 + }, + { + "label": _("Time Required (In Mins)"), + "fieldname": "total_time_in_mins", + "fieldtype": "Float", + "width": 100 + } + ]) + + return columns \ No newline at end of file diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.py b/erpnext/manufacturing/report/production_analytics/production_analytics.py index 7447a1f6705..f62cd255b5e 100644 --- a/erpnext/manufacturing/report/production_analytics/production_analytics.py +++ b/erpnext/manufacturing/report/production_analytics/production_analytics.py @@ -61,7 +61,7 @@ def get_periodic_data(filters, entry): elif getdate(d.planned_start_date) < getdate(from_date) : periodic_data = update_periodic_data(periodic_data, "Overdue", period) - + else: periodic_data = update_periodic_data(periodic_data, "Not Started", period) @@ -148,4 +148,3 @@ def get_chart_data(periodic_data, columns): - diff --git a/erpnext/manufacturing/report/quality_inspection_summary/__init__.py b/erpnext/manufacturing/report/quality_inspection_summary/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.js b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.js new file mode 100644 index 00000000000..d4587aa6619 --- /dev/null +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.js @@ -0,0 +1,40 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Quality Inspection Summary"] = { + "filters": [ + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -12), + reqd: 1 + }, + { + label: __("To Date"), + fieldname:"to_date", + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + }, + { + label: __("Status"), + fieldname: "status", + fieldtype: "Select", + options: ["", "Accepted", "Rejected"] + }, + { + label: __("Item Code"), + fieldname: "item_code", + fieldtype: "Link", + options: "Item" + }, + { + label: __("Inspected By"), + fieldname: "inspected_by", + fieldtype: "Link", + options: "User" + } + ] +}; diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.json b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.json new file mode 100644 index 00000000000..48226e6b21d --- /dev/null +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "creation": "2020-04-26 18:23:53.475110", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "letter_head": "Gadgets International", + "modified": "2020-04-26 18:24:50.529940", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Quality Inspection Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Quality Inspection", + "report_name": "Quality Inspection Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Quality Manager" + }, + { + "role": "Stock User" + }, + { + "role": "Stock Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py new file mode 100644 index 00000000000..6192632bda6 --- /dev/null +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py @@ -0,0 +1,132 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + columns, data = [], [] + data = get_data(filters) + columns = get_columns(filters) + chart_data = get_chart_data(data, filters) + return columns, data , None, chart_data + +def get_data(filters): + query_filters = {"docstatus": ("<", 2)} + + fields = ["name", "status", "report_date", "item_code", "item_name", "sample_size", + "inspection_type", "reference_type", "reference_name", "inspected_by"] + + for field in ["status", "item_code", "status", "inspected_by"]: + if filters.get(field): + query_filters[field] = ("in", filters.get(field)) + + query_filters["report_date"] = (">=", filters.get("from_date")) + query_filters["report_date"] = ("<=", filters.get("to_date")) + + return frappe.get_all("Quality Inspection", + fields= fields, filters=query_filters, order_by="report_date asc") + +def get_chart_data(periodic_data, columns): + labels = ["Rejected", "Accepted"] + + status_wise_data = { + "Accepted": 0, + "Rejected": 0 + } + + datasets = [] + + for d in periodic_data: + status_wise_data[d.status] += 1 + + datasets.append({'name':'Qty Wise Chart', + 'values': [status_wise_data.get("Rejected"), status_wise_data.get("Accepted")]}) + + chart = { + "data": { + 'labels': labels, + 'datasets': datasets + }, + "type": "donut", + "height": 300 + } + + return chart + +def get_columns(filters): + columns = [ + { + "label": _("Id"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Work Order", + "width": 100 + }, + { + "label": _("Report Date"), + "fieldname": "report_date", + "fieldtype": "Date", + "width": 150 + } + ] + + if not filters.get("status"): + columns.append( + { + "label": _("Status"), + "fieldname": "status", + "width": 100 + }, + ) + + columns.extend([ + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 130 + }, + { + "label": _("Item Name"), + "fieldname": "item_name", + "fieldtype": "Data", + "width": 130 + }, + { + "label": _("Sample Size"), + "fieldname": "sample_size", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Inspection Type"), + "fieldname": "inspection_type", + "fieldtype": "Data", + "width": 110 + }, + { + "label": _("Document Type"), + "fieldname": "reference_type", + "fieldtype": "Data", + "width": 90 + }, + { + "label": _("Document Name"), + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "options": "reference_type", + "width": 150 + }, + { + "label": _("Inspected By"), + "fieldname": "inspected_by", + "fieldtype": "Link", + "options": "User", + "width": 150 + } + ]) + + return columns \ No newline at end of file diff --git a/erpnext/manufacturing/report/work_order_summary/__init__.py b/erpnext/manufacturing/report/work_order_summary/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js new file mode 100644 index 00000000000..ec9fe35d63a --- /dev/null +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js @@ -0,0 +1,65 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Work Order Summary"] = { + "filters": [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -12), + reqd: 1 + }, + { + label: __("To Date"), + fieldname:"to_date", + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + }, + { + label: __("Status"), + fieldname: "status", + fieldtype: "Select", + options: ["", "Not Started", "In Process", "Completed", "Stopped"] + }, + { + label: __("Sales Orders"), + fieldname: "sales_order", + fieldtype: "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Sales Order', txt); + } + }, + { + label: __("Production Item"), + fieldname: "production_item", + fieldtype: "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Item', txt); + } + }, + { + label: __("Age"), + fieldname:"age", + fieldtype: "Int", + default: "0" + }, + { + label: __("Charts Based On"), + fieldname:"charts_based_on", + fieldtype: "Select", + options: ["Status", "Age", "Quantity"], + default: "Status" + }, + ] +}; diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.json b/erpnext/manufacturing/report/work_order_summary/work_order_summary.json new file mode 100644 index 00000000000..0d093e22e94 --- /dev/null +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.json @@ -0,0 +1,31 @@ +{ + "add_total_row": 0, + "creation": "2020-04-17 17:07:56.830358", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "Gadgets International", + "modified": "2020-04-19 16:59:47.979278", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Work Order Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Work Order", + "report_name": "Work Order Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Stock User" + }, + { + "role": "Manufacturing Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py new file mode 100644 index 00000000000..bc09ed43354 --- /dev/null +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py @@ -0,0 +1,270 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import date_diff, today, getdate, flt +from frappe import _ +from erpnext.stock.report.stock_analytics.stock_analytics import (get_period_date_ranges, get_period) + +def execute(filters=None): + columns, data = [], [] + + if not filters.get("age"): + filters["age"] = 0 + + data = get_data(filters) + columns = get_columns(filters) + chart_data = get_chart_data(data, filters) + return columns, data, None, chart_data + +def get_data(filters): + query_filters = {"docstatus": 1} + + fields = ["name", "status", "sales_order", "production_item", "qty", "produced_qty", + "planned_start_date", "planned_end_date", "actual_start_date", "actual_end_date", "lead_time"] + + for field in ["sales_order", "production_item", "status", "company"]: + if filters.get(field): + query_filters[field] = ("in", filters.get(field)) + + query_filters["planned_start_date"] = (">=", filters.get("from_date")) + query_filters["planned_end_date"] = ("<=", filters.get("to_date")) + + data = frappe.get_all("Work Order", + fields= fields, filters=query_filters, order_by="planned_start_date asc") + + res = [] + for d in data: + start_date = d.actual_start_date or d.planned_start_date + d.age = 0 + + if d.status != 'Completed': + d.age = date_diff(today(), start_date) + + if filters.get("age") <= d.age: + res.append(d) + + return res + +def get_chart_data(data, filters): + if filters.get("charts_based_on") == "Status": + return get_chart_based_on_status(data) + elif filters.get("charts_based_on") == "Age": + return get_chart_based_on_age(data) + else: + return get_chart_based_on_qty(data, filters) + +def get_chart_based_on_status(data): + labels = ["Not Started", "In Process", "Stopped", "Completed"] + + status_wise_data = { + "Not Started": 0, + "In Process": 0, + "Stopped": 0, + "Completed": 0 + } + + for d in data: + if d.status == "In Process" and d.produced_qty: + status_wise_data["Completed"] += d.produced_qty + + status_wise_data[d.status] += d.qty + + values = [status_wise_data["Not Started"], status_wise_data["In Process"], + status_wise_data["Stopped"], status_wise_data["Completed"]] + + chart = { + "data": { + 'labels': labels, + 'datasets': [{'name':'Qty Wise Chart', 'values': values}] + }, + "type": "donut", + "height": 300 + } + + return chart + +def get_chart_based_on_age(data): + labels = ["0-30 Days", "30-60 Days", "60-90 Days", "90 Above"] + + age_wise_data = { + "0-30 Days": 0, + "30-60 Days": 0, + "60-90 Days": 0, + "90 Above": 0 + } + + for d in data: + if d.age > 0 and d.age <= 30: + age_wise_data["0-30 Days"] += 1 + elif d.age > 30 and d.age <= 60: + age_wise_data["30-60 Days"] += 1 + elif d.age > 60 and d.age <= 90: + age_wise_data["60-90 Days"] += 1 + else: + age_wise_data["90 Above"] += 1 + + values = [age_wise_data["0-30 Days"], age_wise_data["30-60 Days"], + age_wise_data["60-90 Days"], age_wise_data["90 Above"]] + + chart = { + "data": { + 'labels': labels, + 'datasets': [{'name':'Qty Wise Chart', 'values': values}] + }, + "type": "donut", + "height": 300 + } + + return chart + +def get_chart_based_on_qty(data, filters): + labels, periodic_data = prepare_chart_data(data, filters) + + pending, completed = [], [] + datasets = [] + + for d in labels: + pending.append(periodic_data.get("Pending").get(d)) + completed.append(periodic_data.get("Completed").get(d)) + + datasets.append({"name": "Pending", "values": pending}) + datasets.append({"name": "Completed", "values": completed}) + + chart = { + "data": { + 'labels': labels, + 'datasets': datasets + }, + "type": "bar", + "barOptions": { + "stacked": 1 + } + } + + return chart + +def prepare_chart_data(data, filters): + labels = [] + + periodic_data = { + "Pending": {}, + "Completed": {} + } + + filters.range = "Monthly" + + ranges = get_period_date_ranges(filters) + for from_date, end_date in ranges: + period = get_period(end_date, filters) + if period not in labels: + labels.append(period) + + if period not in periodic_data["Pending"]: + periodic_data["Pending"][period] = 0 + + if period not in periodic_data["Completed"]: + periodic_data["Completed"][period] = 0 + + for d in data: + if getdate(d.planned_start_date) >= from_date and getdate(d.planned_start_date) <= end_date: + periodic_data["Pending"][period] += (flt(d.qty) - flt(d.produced_qty)) + periodic_data["Completed"][period] += flt(d.produced_qty) + + return labels, periodic_data + +def get_columns(filters): + columns = [ + { + "label": _("Id"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Work Order", + "width": 100 + }, + ] + + if not filters.get("status"): + columns.append( + { + "label": _("Status"), + "fieldname": "status", + "width": 100 + }, + ) + + columns.extend([ + { + "label": _("Production Item"), + "fieldname": "production_item", + "fieldtype": "Link", + "options": "Item", + "width": 130 + }, + { + "label": _("Produce Qty"), + "fieldname": "qty", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Produced Qty"), + "fieldname": "produced_qty", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Sales Order"), + "fieldname": "sales_order", + "fieldtype": "Link", + "options": "Sales Order", + "width": 90 + }, + { + "label": _("Planned Start Date"), + "fieldname": "planned_start_date", + "fieldtype": "Date", + "width": 150 + }, + { + "label": _("Planned End Date"), + "fieldname": "planned_end_date", + "fieldtype": "Date", + "width": 150 + } + ]) + + if filters.get("status") != 'Not Started': + columns.extend([ + { + "label": _("Actual Start Date"), + "fieldname": "actual_start_date", + "fieldtype": "Date", + "width": 100 + }, + { + "label": _("Actual End Date"), + "fieldname": "actual_end_date", + "fieldtype": "Date", + "width": 100 + }, + { + "label": _("Age"), + "fieldname": "age", + "fieldtype": "Float", + "width": 110 + }, + ]) + + if filters.get("status") == 'Completed': + columns.extend([ + { + "label": _("Lead Time (in mins)"), + "fieldname": "lead_time", + "fieldtype": "Float", + "width": 110 + }, + ]) + + return columns \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7b66a2d830c..dc88ffb6bea 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -687,3 +687,4 @@ erpnext.patches.v12_0.set_valid_till_date_in_supplier_quotation erpnext.patches.v12_0.set_serial_no_status erpnext.patches.v12_0.update_price_list_currency_in_bom execute:frappe.delete_doc_if_exists('Dashboard', 'Accounts') +erpnext.patches.v13_0.update_actual_start_and_end_date_in_wo diff --git a/erpnext/patches/v13_0/__init__.py b/erpnext/patches/v13_0/__init__.py index e69de29bb2d..baffc488252 100644 --- a/erpnext/patches/v13_0/__init__.py +++ b/erpnext/patches/v13_0/__init__.py @@ -0,0 +1 @@ +from __future__ import unicode_literals diff --git a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py new file mode 100644 index 00000000000..331c5590e58 --- /dev/null +++ b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py @@ -0,0 +1,42 @@ + +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe +from frappe.utils import add_to_date +from frappe.utils.dashboard import get_config, make_records + +def execute(): + frappe.reload_doc("manufacturing", "doctype", "work_order") + frappe.reload_doc("manufacturing", "doctype", "work_order_item") + frappe.reload_doc("manufacturing", "doctype", "job_card") + + data = frappe.get_all("Work Order", + filters = { + "docstatus": 1, + "status": ("in", ["In Process", "Completed"]) + }) + + for d in data: + doc = frappe.get_doc("Work Order", d.name) + doc.set_actual_dates() + doc.db_set("actual_start_date", doc.actual_start_date, update_modified=False) + + if doc.status == "Completed": + frappe.db.set_value("Work Order", d.name, { + "actual_end_date": doc.actual_end_date, + "lead_time": doc.lead_time + }, update_modified=False) + + if not doc.planned_end_date: + planned_end_date = add_to_date(doc.planned_start_date, minutes=doc.lead_time) + doc.db_set("planned_end_date", doc.actual_start_date, update_modified=False) + + frappe.db.sql(""" UPDATE `tabJob Card` as jc, `tabWork Order` as wo + SET + jc.production_item = wo.production_item, jc.item_name = wo.item_name + WHERE + jc.work_order = wo.name and IFNULL(jc.production_item, "") = "" + """) \ No newline at end of file diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json index a9f3cd09ef5..c951066aa83 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2013-04-30 13:13:03", "doctype": "DocType", @@ -8,6 +9,7 @@ "field_order": [ "naming_series", "report_date", + "status", "column_break_4", "inspection_type", "reference_type", @@ -20,17 +22,16 @@ "column_break1", "item_name", "description", - "status", + "bom_no", + "specification_details", + "quality_inspection_template", + "readings", "section_break_14", "inspected_by", "verified_by", - "bom_no", "column_break_17", "remarks", - "amended_from", - "specification_details", - "quality_inspection_template", - "readings" + "amended_from" ], "fields": [ { @@ -231,7 +232,8 @@ "icon": "fa fa-search", "idx": 1, "is_submittable": 1, - "modified": "2019-07-12 12:07:23.153698", + "links": [], + "modified": "2020-04-26 17:50:25.068222", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index be2dd526a65..18d68539daa 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -732,11 +732,15 @@ class StockEntry(StockController): pro_doc = frappe.get_doc("Work Order", self.work_order) _validate_work_order(pro_doc) pro_doc.run_method("update_status") + if self.fg_completed_qty: pro_doc.run_method("update_work_order_qty") if self.purpose == "Manufacture": pro_doc.run_method("update_planned_qty") + if not pro_doc.operations: + pro_doc.set_actual_dates() + def get_item_details(self, args=None, for_update=False): item = frappe.db.sql("""select i.name, i.stock_uom, i.description, i.image, i.item_name, i.item_group, i.has_batch_no, i.sample_quantity, i.has_serial_no,