Merge pull request #19441 from rohitwaghchaure/capacity_planning_for_workstataion_feature

fix: capacity planning back
This commit is contained in:
rohitwaghchaure 2019-12-03 15:23:06 +05:30 committed by GitHub
commit 36b60dbbef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 788 additions and 2068 deletions

File diff suppressed because it is too large Load Diff

View File

@ -4,10 +4,16 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import datetime
from frappe import _ from frappe import _
from frappe.utils import flt, time_diff_in_hours, get_datetime
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate,
get_time, add_to_date, time_diff, add_days, get_datetime_str)
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
class OverlapError(frappe.ValidationError): pass
class JobCard(Document): class JobCard(Document):
def validate(self): def validate(self):
@ -26,7 +32,7 @@ class JobCard(Document):
data = self.get_overlap_for(d) data = self.get_overlap_for(d)
if data: if data:
frappe.throw(_("Row {0}: From Time and To Time of {1} is overlapping with {2}") frappe.throw(_("Row {0}: From Time and To Time of {1} is overlapping with {2}")
.format(d.idx, self.name, data.name)) .format(d.idx, self.name, data.name), OverlapError)
if d.from_time and d.to_time: if d.from_time and d.to_time:
d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60 d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60
@ -35,27 +41,120 @@ class JobCard(Document):
if d.completed_qty: if d.completed_qty:
self.total_completed_qty += d.completed_qty self.total_completed_qty += d.completed_qty
def get_overlap_for(self, args): def get_overlap_for(self, args, check_next_available_slot=False):
existing = frappe.db.sql("""select jc.name as name from production_capacity = 1
if self.workstation:
production_capacity = frappe.get_cached_value("Workstation",
self.workstation, 'production_capacity') or 1
validate_overlap_for = " and jc.workstation = %(workstation)s "
if self.employee:
# override capacity for employee
production_capacity = 1
validate_overlap_for = " and jc.employee = %(employee)s "
extra_cond = ''
if check_next_available_slot:
extra_cond = " or (%(from_time)s <= jctl.from_time and %(to_time)s <= jctl.to_time)"
existing = frappe.db.sql("""select jc.name as name, jctl.to_time from
`tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and `tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and
( (
(%(from_time)s > jctl.from_time and %(from_time)s < jctl.to_time) or (%(from_time)s > jctl.from_time and %(from_time)s < jctl.to_time) or
(%(to_time)s > jctl.from_time and %(to_time)s < jctl.to_time) or (%(to_time)s > jctl.from_time and %(to_time)s < jctl.to_time) or
(%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time)) (%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time) {0}
and jctl.name!=%(name)s )
and jc.name!=%(parent)s and jctl.name != %(name)s and jc.name != %(parent)s and jc.docstatus < 2 {1}
and jc.docstatus < 2 order by jctl.to_time desc limit 1""".format(extra_cond, validate_overlap_for),
and jc.employee = %(employee)s """,
{ {
"from_time": args.from_time, "from_time": args.from_time,
"to_time": args.to_time, "to_time": args.to_time,
"name": args.name or "No Name", "name": args.name or "No Name",
"parent": args.parent or "No Name", "parent": args.parent or "No Name",
"employee": self.employee "employee": self.employee,
"workstation": self.workstation
}, as_dict=True) }, as_dict=True)
if existing and production_capacity > len(existing):
return
return existing[0] if existing else None return existing[0] if existing else None
def schedule_time_logs(self, row):
row.remaining_time_in_mins = row.time_in_mins
while row.remaining_time_in_mins > 0:
args = frappe._dict({
"from_time": row.planned_start_time,
"to_time": row.planned_end_time
})
self.validate_overlap_for_workstation(args, row)
self.check_workstation_time(row)
def validate_overlap_for_workstation(self, args, row):
# get the last record based on the to time from the job card
data = self.get_overlap_for(args, check_next_available_slot=True)
if data:
row.planned_start_time = get_datetime(data.to_time + get_mins_between_operations())
def check_workstation_time(self, row):
workstation_doc = frappe.get_cached_doc("Workstation", self.workstation)
if (not workstation_doc.working_hours or
cint(frappe.db.get_single_value("Manufacturing Settings", "allow_overtime"))):
row.remaining_time_in_mins -= time_diff_in_minutes(row.planned_end_time,
row.planned_start_time)
self.update_time_logs(row)
return
start_date = getdate(row.planned_start_time)
start_time = get_time(row.planned_start_time)
new_start_date = workstation_doc.validate_workstation_holiday(start_date)
if new_start_date != start_date:
row.planned_start_time = datetime.datetime.combine(new_start_date, start_time)
start_date = new_start_date
total_idx = len(workstation_doc.working_hours)
for i, time_slot in enumerate(workstation_doc.working_hours):
workstation_start_time = datetime.datetime.combine(start_date, get_time(time_slot.start_time))
workstation_end_time = datetime.datetime.combine(start_date, get_time(time_slot.end_time))
if (get_datetime(row.planned_start_time) >= workstation_start_time and
get_datetime(row.planned_start_time) <= workstation_end_time):
time_in_mins = time_diff_in_minutes(workstation_end_time, row.planned_start_time)
# If remaining time fit in workstation time logs else split hours as per workstation time
if time_in_mins > row.remaining_time_in_mins:
row.planned_end_time = add_to_date(row.planned_start_time,
minutes=row.remaining_time_in_mins)
row.remaining_time_in_mins = 0
else:
row.planned_end_time = add_to_date(row.planned_start_time, minutes=time_in_mins)
row.remaining_time_in_mins -= time_in_mins
self.update_time_logs(row)
if total_idx != (i+1) and row.remaining_time_in_mins > 0:
row.planned_start_time = datetime.datetime.combine(start_date,
get_time(workstation_doc.working_hours[i+1].start_time))
if row.remaining_time_in_mins > 0:
start_date = add_days(start_date, 1)
row.planned_start_time = datetime.datetime.combine(start_date,
get_time(workstation_doc.working_hours[0].start_time))
def update_time_logs(self, row):
self.append("time_logs", {
"from_time": row.planned_start_time,
"to_time": row.planned_end_time,
"completed_qty": 0,
"time_in_mins": time_diff_in_minutes(row.planned_end_time, row.planned_start_time),
})
def get_required_items(self): def get_required_items(self):
if not self.get('work_order'): if not self.get('work_order'):
return return
@ -251,3 +350,6 @@ def make_stock_entry(source_name, target_doc=None):
}, target_doc, set_missing_values) }, target_doc, set_missing_values)
return doclist return doclist
def time_diff_in_minutes(string_ed_date, string_st_date):
return time_diff(string_ed_date, string_st_date).total_seconds() / 60

View File

@ -1,585 +1,178 @@
{ {
"allow_copy": 0, "creation": "2014-11-27 14:12:07.542534",
"allow_guest_to_view": 0, "doctype": "DocType",
"allow_import": 0, "document_type": "Document",
"allow_rename": 0, "engine": "InnoDB",
"beta": 0, "field_order": [
"creation": "2014-11-27 14:12:07.542534", "raw_materials_consumption_section",
"custom": 0, "material_consumption",
"docstatus": 0, "column_break_3",
"doctype": "DocType", "backflush_raw_materials_based_on",
"document_type": "Document", "capacity_planning",
"editable_grid": 0, "disable_capacity_planning",
"engine": "InnoDB", "allow_overtime",
"allow_production_on_holidays",
"column_break_5",
"capacity_planning_for_days",
"mins_between_operations",
"section_break_6",
"default_wip_warehouse",
"default_fg_warehouse",
"column_break_11",
"default_scrap_warehouse",
"over_production_for_sales_and_work_order_section",
"overproduction_percentage_for_sales_order",
"column_break_16",
"overproduction_percentage_for_work_order",
"other_settings_section",
"update_bom_costs_automatically"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "capacity_planning",
"allow_in_quick_entry": 0, "fieldtype": "Section Break",
"allow_on_submit": 0, "label": "Capacity Planning"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "capacity_planning",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Capacity Planning",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0, "depends_on": "eval:!doc.disable_capacity_planning",
"allow_on_submit": 0, "description": "Plan time logs outside Workstation Working Hours.",
"bold": 0, "fieldname": "allow_overtime",
"collapsible": 0, "fieldtype": "Check",
"columns": 0, "label": "Allow Overtime"
"description": "Disables creation of time logs against Work Orders. Operations shall not be tracked against Work Order", },
"fieldname": "disable_capacity_planning",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Disable Capacity Planning and Time Tracking",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0, "depends_on": "eval:!doc.disable_capacity_planning",
"allow_on_submit": 0, "fieldname": "allow_production_on_holidays",
"bold": 0, "fieldtype": "Check",
"collapsible": 0, "in_list_view": 1,
"columns": 0, "label": "Allow Production on Holidays"
"description": "Plan time logs outside Workstation Working Hours.", },
"fieldname": "allow_overtime",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Allow Overtime",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_3",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "allow_production_on_holidays",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Allow Production on Holidays",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "default": "30",
"allow_in_quick_entry": 0, "depends_on": "eval:!doc.disable_capacity_planning",
"allow_on_submit": 0, "description": "Try planning operations for X days in advance.",
"bold": 0, "fieldname": "capacity_planning_for_days",
"collapsible": 0, "fieldtype": "Int",
"columns": 0, "label": "Capacity Planning For (Days)"
"fieldname": "column_break_3", },
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "depends_on": "eval:!doc.disable_capacity_planning",
"allow_in_quick_entry": 0, "description": "Default 10 mins",
"allow_on_submit": 0, "fieldname": "mins_between_operations",
"bold": 0, "fieldtype": "Int",
"collapsible": 0, "label": "Time Between Operations (in mins)"
"columns": 0, },
"default": "30",
"description": "Try planning operations for X days in advance.",
"fieldname": "capacity_planning_for_days",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Capacity Planning For (Days)",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_6",
"allow_in_quick_entry": 0, "fieldtype": "Section Break",
"allow_on_submit": 0, "label": "Default Warehouses for Production"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"description": "Default 10 mins",
"fieldname": "mins_between_operations",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Time Between Operations (in mins)",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "overproduction_percentage_for_sales_order",
"allow_in_quick_entry": 0, "fieldtype": "Percent",
"allow_on_submit": 0, "label": "Overproduction Percentage For Sales Order"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "overproduction_percentage_for_work_order",
"allow_in_quick_entry": 0, "fieldtype": "Percent",
"allow_on_submit": 0, "label": "Overproduction Percentage For Work Order"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "overproduction_percentage_for_sales_order",
"fieldtype": "Percent",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Overproduction Percentage For Sales Order",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "default": "BOM",
"allow_in_quick_entry": 0, "fieldname": "backflush_raw_materials_based_on",
"allow_on_submit": 0, "fieldtype": "Select",
"bold": 0, "label": "Backflush Raw Materials Based On",
"collapsible": 0, "options": "BOM\nMaterial Transferred for Manufacture"
"columns": 0, },
"fieldname": "overproduction_percentage_for_work_order",
"fieldtype": "Percent",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Overproduction Percentage For Work Order",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0, "description": "Allow multiple Material Consumption against a Work Order",
"allow_on_submit": 0, "fieldname": "material_consumption",
"bold": 0, "fieldtype": "Check",
"collapsible": 0, "label": "Allow Multiple Material Consumption"
"columns": 0, },
"default": "BOM",
"fieldname": "backflush_raw_materials_based_on",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Backflush Raw Materials Based On",
"length": 0,
"no_copy": 0,
"options": "BOM\nMaterial Transferred for Manufacture",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0, "description": "Update BOM cost automatically via Scheduler, based on latest valuation rate / price list rate / last purchase rate of raw materials.",
"allow_on_submit": 0, "fieldname": "update_bom_costs_automatically",
"bold": 0, "fieldtype": "Check",
"collapsible": 0, "label": "Update BOM Cost Automatically"
"columns": 0, },
"description": "Allow multiple Material Consumption against a Work Order",
"fieldname": "material_consumption",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Allow Multiple Material Consumption",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_11",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Update BOM cost automatically via Scheduler, based on latest valuation rate / price list rate / last purchase rate of raw materials.",
"fieldname": "update_bom_costs_automatically",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Update BOM Cost Automatically",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "default_wip_warehouse",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "Default Work In Progress Warehouse",
"bold": 0, "options": "Warehouse"
"collapsible": 0, },
"columns": 0,
"fieldname": "column_break_11",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "default_fg_warehouse",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "Default Finished Goods Warehouse",
"bold": 0, "options": "Warehouse"
"collapsible": 0, },
"columns": 0,
"fieldname": "default_wip_warehouse",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Default Work In Progress Warehouse",
"length": 0,
"no_copy": 0,
"options": "Warehouse",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0, "fieldname": "disable_capacity_planning",
"allow_on_submit": 0, "fieldtype": "Check",
"bold": 0, "label": "Disable Capacity Planning"
"collapsible": 0, },
"columns": 0, {
"fieldname": "default_fg_warehouse", "fieldname": "default_scrap_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0, "label": "Default Scrap Warehouse",
"ignore_user_permissions": 0, "options": "Warehouse"
"ignore_xss_filter": 0, },
"in_filter": 0, {
"in_global_search": 0, "fieldname": "over_production_for_sales_and_work_order_section",
"in_list_view": 0, "fieldtype": "Section Break",
"in_standard_filter": 0, "label": "Over Production for Sales and Work Order"
"label": "Default Finished Goods Warehouse", },
"length": 0, {
"no_copy": 0, "fieldname": "raw_materials_consumption_section",
"options": "Warehouse", "fieldtype": "Section Break",
"permlevel": 0, "label": "Raw Materials Consumption"
"precision": "", },
"print_hide": 0, {
"print_hide_if_no_value": 0, "fieldname": "column_break_16",
"read_only": 0, "fieldtype": "Column Break"
"remember_last_selected_value": 0, },
"report_hide": 0, {
"reqd": 0, "fieldname": "other_settings_section",
"search_index": 0, "fieldtype": "Section Break",
"set_only_once": 0, "label": "Other Settings"
"translatable": 0, },
"unique": 0 {
"fieldname": "column_break_5",
"fieldtype": "Column Break"
} }
], ],
"has_web_view": 0, "icon": "icon-wrench",
"hide_heading": 0, "issingle": 1,
"hide_toolbar": 0, "modified": "2019-11-26 13:10:45.569341",
"icon": "icon-wrench", "modified_by": "Administrator",
"idx": 0, "module": "Manufacturing",
"image_view": 0, "name": "Manufacturing Settings",
"in_create": 0, "owner": "Administrator",
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
"modified": "2018-05-28 00:46:25.310621",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",
"name_case": "",
"owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0, "create": 1,
"cancel": 0, "read": 1,
"create": 1, "role": "Manufacturing Manager",
"delete": 0, "share": 1,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Manufacturing Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0, "sort_field": "modified",
"read_only": 0, "sort_order": "DESC",
"read_only_onload": 0, "track_changes": 1
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
} }

View File

@ -5,10 +5,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest import unittest
import frappe import frappe
from frappe.utils import flt, time_diff_in_hours, now, add_days, cint from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.manufacturing.doctype.work_order.work_order \ from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry,
import make_stock_entry, ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError)
from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.stock.utils import get_bin from erpnext.stock.utils import get_bin
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
@ -307,14 +307,50 @@ class TestWorkOrder(unittest.TestCase):
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
if data: if data:
frappe.db.set_value("Manufacturing Settings",
None, "disable_capacity_planning", 0)
bom, bom_item = data bom, bom_item = data
bom_doc = frappe.get_doc('BOM', bom) bom_doc = frappe.get_doc('BOM', bom)
work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom)
self.assertTrue(work_order.planned_end_date)
job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
self.assertEqual(len(job_cards), len(bom_doc.operations)) self.assertEqual(len(job_cards), len(bom_doc.operations))
def test_capcity_planning(self):
frappe.db.set_value("Manufacturing Settings", None, {
"disable_capacity_planning": 0,
"capacity_planning_for_days": 1
})
data = frappe.get_cached_value('BOM', {'docstatus': 1, 'item': '_Test FG Item 2',
'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
if data:
bom, bom_item = data
planned_start_date = add_months(today(), months=-1)
work_order = make_wo_order_test_record(item=bom_item,
qty=10, bom_no=bom, planned_start_date=planned_start_date)
work_order1 = make_wo_order_test_record(item=bom_item,
qty=30, bom_no=bom, planned_start_date=planned_start_date, do_not_submit=1)
self.assertRaises(CapacityError, work_order1.submit)
frappe.db.set_value("Manufacturing Settings", None, {
"capacity_planning_for_days": 30
})
work_order1.reload()
work_order1.submit()
self.assertTrue(work_order1.docstatus, 1)
work_order1.cancel()
work_order.cancel()
def test_work_order_with_non_transfer_item(self): def test_work_order_with_non_transfer_item(self):
items = {'Finished Good Transfer Item': 1, '_Test FG Item': 1, '_Test FG Item 1': 0} items = {'Finished Good Transfer Item': 1, '_Test FG Item': 1, '_Test FG Item 1': 0}
for item, allow_transfer in items.items(): for item, allow_transfer in items.items():
@ -371,14 +407,12 @@ def make_wo_order_test_record(**args):
wo_order.skip_transfer=1 wo_order.skip_transfer=1
wo_order.get_items_and_operations_from_bom() wo_order.get_items_and_operations_from_bom()
wo_order.sales_order = args.sales_order or None wo_order.sales_order = args.sales_order or None
wo_order.planned_start_date = args.planned_start_date or now()
if args.source_warehouse: if args.source_warehouse:
for item in wo_order.get("required_items"): for item in wo_order.get("required_items"):
item.source_warehouse = args.source_warehouse item.source_warehouse = args.source_warehouse
if args.planned_start_date:
wo_order.planned_start_date = args.planned_start_date
if not args.do_not_save: if not args.do_not_save:
wo_order.insert() wo_order.insert()

View File

@ -551,6 +551,7 @@ erpnext.work_order = {
if (!r.exe) { if (!r.exe) {
frm.set_value("wip_warehouse", r.message.wip_warehouse); frm.set_value("wip_warehouse", r.message.wip_warehouse);
frm.set_value("fg_warehouse", r.message.fg_warehouse); frm.set_value("fg_warehouse", r.message.fg_warehouse);
frm.set_value("scrap_warehouse", r.message.scrap_warehouse);
} }
} }
}); });

View File

@ -12,7 +12,7 @@ from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_bom_items
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from erpnext.stock.doctype.item.item import validate_end_of_life from erpnext.stock.doctype.item.item import validate_end_of_life
from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError
from erpnext.projects.doctype.timesheet.timesheet import OverlapError from erpnext.manufacturing.doctype.job_card.job_card import OverlapError
from erpnext.stock.doctype.stock_entry.stock_entry import get_additional_costs from erpnext.stock.doctype.stock_entry.stock_entry import get_additional_costs
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty
@ -22,6 +22,7 @@ from erpnext.utilities.transaction_base import validate_uom_is_integer
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
class OverProductionError(frappe.ValidationError): pass class OverProductionError(frappe.ValidationError): pass
class CapacityError(frappe.ValidationError): pass
class StockOverProductionError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass
class OperationTooLongError(frappe.ValidationError): pass class OperationTooLongError(frappe.ValidationError): pass
class ItemHasVariantError(frappe.ValidationError): pass class ItemHasVariantError(frappe.ValidationError): pass
@ -260,12 +261,50 @@ class WorkOrder(Document):
self.update_reserved_qty_for_production() self.update_reserved_qty_for_production()
def create_job_card(self): def create_job_card(self):
for row in self.operations: manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings")
enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning)
plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30
for i, row in enumerate(self.operations):
self.set_operation_start_end_time(i, row)
if not row.workstation: if not row.workstation:
frappe.throw(_("Row {0}: select the workstation against the operation {1}") frappe.throw(_("Row {0}: select the workstation against the operation {1}")
.format(row.idx, row.operation)) .format(row.idx, row.operation))
create_job_card(self, row, auto_create=True) original_start_time = row.planned_start_time
job_card_doc = create_job_card(self, row,
enable_capacity_planning=enable_capacity_planning, auto_create=True)
if enable_capacity_planning and job_card_doc:
row.planned_start_time = job_card_doc.time_logs[0].from_time
row.planned_end_time = job_card_doc.time_logs[-1].to_time
if date_diff(row.planned_start_time, original_start_time) > plan_days:
frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.")
.format(plan_days, row.operation), CapacityError)
row.db_update()
planned_end_date = self.operations and self.operations[-1].planned_end_time
if planned_end_date:
self.db_set("planned_end_date", planned_end_date)
def set_operation_start_end_time(self, idx, row):
"""Set start and end time for given operation. If first operation, set start as
`planned_start_date`, else add time diff to end time of earlier operation."""
if idx==0:
# first operation at planned_start date
row.planned_start_time = self.planned_start_date
else:
row.planned_start_time = get_datetime(self.operations[idx-1].planned_end_time)\
+ get_mins_between_operations()
row.planned_end_time = get_datetime(row.planned_start_time) + relativedelta(minutes = row.time_in_mins)
if row.planned_start_time == row.planned_end_time:
frappe.throw(_("Capacity Planning Error, planned start time can not be same as end time"))
def validate_cancel(self): def validate_cancel(self):
if self.status == "Stopped": if self.status == "Stopped":
@ -327,9 +366,8 @@ class WorkOrder(Document):
"""Fetch operations from BOM and set in 'Work Order'""" """Fetch operations from BOM and set in 'Work Order'"""
self.set('operations', []) self.set('operations', [])
if not self.bom_no \ if not self.bom_no:
or cint(frappe.db.get_single_value("Manufacturing Settings", "disable_capacity_planning")): return
return
if self.use_multi_level_bom: if self.use_multi_level_bom:
bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree() bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree()
@ -681,11 +719,13 @@ def make_stock_entry(work_order_id, purpose, qty=None):
@frappe.whitelist() @frappe.whitelist()
def get_default_warehouse(): def get_default_warehouse():
wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", doc = frappe.get_cached_doc("Manufacturing Settings")
"default_wip_warehouse")
fg_warehouse = frappe.db.get_single_value("Manufacturing Settings", return {
"default_fg_warehouse") "wip_warehouse": doc.default_wip_warehouse,
return {"wip_warehouse": wip_warehouse, "fg_warehouse": fg_warehouse} "fg_warehouse": doc.default_fg_warehouse,
"scrap_warehouse": doc.default_scrap_warehouse
}
@frappe.whitelist() @frappe.whitelist()
def stop_unstop(work_order, status): def stop_unstop(work_order, status):
@ -721,7 +761,7 @@ def make_job_card(work_order, operation, workstation, qty=0):
if row: if row:
return create_job_card(work_order, row, qty) return create_job_card(work_order, row, qty)
def create_job_card(work_order, row, qty=0, auto_create=False): def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto_create=False):
doc = frappe.new_doc("Job Card") doc = frappe.new_doc("Job Card")
doc.update({ doc.update({
'work_order': work_order.name, 'work_order': work_order.name,
@ -741,6 +781,9 @@ def create_job_card(work_order, row, qty=0, auto_create=False):
if auto_create: if auto_create:
doc.flags.ignore_mandatory = True doc.flags.ignore_mandatory = True
if enable_capacity_planning:
doc.schedule_time_logs(row)
doc.insert() doc.insert()
frappe.msgprint(_("Job card {0} created").format(doc.name)) frappe.msgprint(_("Job card {0} created").format(doc.name))

View File

@ -1,466 +1,159 @@
{ {
"allow_copy": 0, "allow_import": 1,
"allow_guest_to_view": 0, "allow_rename": 1,
"allow_import": 1, "autoname": "field:workstation_name",
"allow_rename": 1, "creation": "2013-01-10 16:34:17",
"autoname": "field:workstation_name", "doctype": "DocType",
"beta": 0, "document_type": "Setup",
"creation": "2013-01-10 16:34:17", "field_order": [
"custom": 0, "workstation_name",
"docstatus": 0, "production_capacity",
"doctype": "DocType", "column_break_3",
"document_type": "Setup", "over_heads",
"editable_grid": 0, "hour_rate_electricity",
"hour_rate_consumable",
"column_break_11",
"hour_rate_rent",
"hour_rate_labour",
"hour_rate",
"working_hours_section",
"holiday_list",
"working_hours",
"workstaion_description",
"description"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "workstation_name",
"allow_on_submit": 0, "fieldtype": "Data",
"bold": 0, "in_list_view": 1,
"collapsible": 1, "label": "Workstation Name",
"columns": 0, "oldfieldname": "workstation_name",
"fieldname": "description_section", "oldfieldtype": "Data",
"fieldtype": "Section Break", "reqd": 1,
"hidden": 0, "unique": 1
"ignore_user_permissions": 0, },
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "description",
"allow_on_submit": 0, "fieldtype": "Text",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Description",
"columns": 0, "oldfieldname": "description",
"fieldname": "workstation_name", "oldfieldtype": "Text",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Workstation Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "workstation_name",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"oldfieldname": "description",
"oldfieldtype": "Text",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "300px" "width": "300px"
}, },
{ {
"allow_bulk_edit": 0, "fieldname": "over_heads",
"allow_on_submit": 0, "fieldtype": "Section Break",
"bold": 0, "label": "Operating Costs",
"collapsible": 0, "oldfieldtype": "Section Break"
"columns": 0, },
"fieldname": "over_heads",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Operating Costs",
"length": 0,
"no_copy": 0,
"oldfieldtype": "Section Break",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "bold": 1,
"allow_on_submit": 0, "description": "per hour",
"bold": 0, "fieldname": "hour_rate_electricity",
"collapsible": 0, "fieldtype": "Currency",
"columns": 0, "label": "Electricity Cost",
"description": "per hour", "oldfieldname": "hour_rate_electricity",
"fieldname": "hour_rate_electricity", "oldfieldtype": "Currency"
"fieldtype": "Currency", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Electricity Cost",
"length": 0,
"no_copy": 0,
"oldfieldname": "hour_rate_electricity",
"oldfieldtype": "Currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "bold": 1,
"allow_on_submit": 0, "description": "per hour",
"bold": 0, "fieldname": "hour_rate_consumable",
"collapsible": 0, "fieldtype": "Currency",
"columns": 0, "label": "Consumable Cost",
"description": "per hour", "oldfieldname": "hour_rate_consumable",
"fieldname": "hour_rate_consumable", "oldfieldtype": "Currency"
"fieldtype": "Currency", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Consumable Cost",
"length": 0,
"no_copy": 0,
"oldfieldname": "hour_rate_consumable",
"oldfieldtype": "Currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_11",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_11",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "bold": 1,
"allow_on_submit": 0, "description": "per hour",
"bold": 0, "fieldname": "hour_rate_rent",
"collapsible": 0, "fieldtype": "Currency",
"columns": 0, "label": "Rent Cost",
"description": "per hour", "oldfieldname": "hour_rate_rent",
"fieldname": "hour_rate_rent", "oldfieldtype": "Currency"
"fieldtype": "Currency", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Rent Cost",
"length": 0,
"no_copy": 0,
"oldfieldname": "hour_rate_rent",
"oldfieldtype": "Currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "bold": 1,
"allow_on_submit": 0, "description": "Wages per hour",
"bold": 0, "fieldname": "hour_rate_labour",
"collapsible": 0, "fieldtype": "Currency",
"columns": 0, "label": "Wages",
"description": "Wages per hour", "oldfieldname": "hour_rate_labour",
"fieldname": "hour_rate_labour", "oldfieldtype": "Currency"
"fieldtype": "Currency", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Wages",
"length": 0,
"no_copy": 0,
"oldfieldname": "hour_rate_labour",
"oldfieldtype": "Currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "description": "per hour",
"allow_on_submit": 0, "fieldname": "hour_rate",
"bold": 0, "fieldtype": "Currency",
"collapsible": 0, "label": "Net Hour Rate",
"columns": 0, "oldfieldname": "hour_rate",
"description": "per hour", "oldfieldtype": "Currency",
"fieldname": "hour_rate", "read_only": 1
"fieldtype": "Currency", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Net Hour Rate",
"length": 0,
"no_copy": 0,
"oldfieldname": "hour_rate",
"oldfieldtype": "Currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "working_hours_section",
"allow_on_submit": 0, "fieldtype": "Section Break",
"bold": 0, "label": "Working Hours"
"collapsible": 0, },
"columns": 0,
"fieldname": "working_hours_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Working Hours",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "working_hours",
"allow_on_submit": 0, "fieldtype": "Table",
"bold": 0, "label": "Working Hours",
"collapsible": 0, "options": "Workstation Working Hour"
"columns": 0, },
"fieldname": "working_hours",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Working Hours",
"length": 0,
"no_copy": 0,
"options": "Workstation Working Hour",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "holiday_list",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "label": "Holiday List",
"collapsible": 0, "options": "Holiday List"
"columns": 0, },
"default": "", {
"fieldname": "holiday_list", "default": "1",
"fieldtype": "Link", "fieldname": "production_capacity",
"hidden": 0, "fieldtype": "Int",
"ignore_user_permissions": 0, "label": "Production Capacity",
"ignore_xss_filter": 0, "reqd": 1
"in_filter": 0, },
"in_global_search": 0, {
"in_list_view": 0, "fieldname": "column_break_3",
"in_standard_filter": 0, "fieldtype": "Column Break"
"label": "Holiday List", },
"length": 0, {
"no_copy": 0, "collapsible": 1,
"options": "Holiday List", "fieldname": "workstaion_description",
"permlevel": 0, "fieldtype": "Section Break",
"precision": "", "label": "Description"
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "icon": "icon-wrench",
"hide_heading": 0, "idx": 1,
"hide_toolbar": 0, "modified": "2019-11-26 12:39:19.742052",
"icon": "icon-wrench", "modified_by": "Administrator",
"idx": 1, "module": "Manufacturing",
"image_view": 0, "name": "Workstation",
"in_create": 0, "owner": "Administrator",
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-07-18 22:28:50.163219",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation",
"owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0, "create": 1,
"apply_user_permissions": 0, "delete": 1,
"cancel": 0, "email": 1,
"create": 1, "print": 1,
"delete": 1, "read": 1,
"email": 1, "report": 1,
"export": 0, "role": "Manufacturing User",
"if_owner": 0, "share": 1,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Manufacturing User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0, "show_name_in_global_search": 1,
"read_only_onload": 0, "sort_order": "ASC",
"show_name_in_global_search": 1, "track_changes": 1
"sort_order": "ASC",
"track_changes": 1,
"track_seen": 0
} }

View File

@ -4,7 +4,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt, cint, getdate, formatdate, comma_and, time_diff_in_seconds, to_timedelta from erpnext.support.doctype.issue.issue import get_holidays
from frappe.utils import (flt, cint, getdate, formatdate,
comma_and, time_diff_in_seconds, to_timedelta, add_days)
from frappe.model.document import Document from frappe.model.document import Document
from dateutil.parser import parse from dateutil.parser import parse
@ -43,6 +45,17 @@ class Workstation(Document):
where parent = %s and workstation = %s""", where parent = %s and workstation = %s""",
(self.hour_rate, bom_no[0], self.name)) (self.hour_rate, bom_no[0], self.name))
def validate_workstation_holiday(self, schedule_date, skip_holiday_list_check=False):
if not skip_holiday_list_check and (not self.holiday_list or
cint(frappe.db.get_single_value("Manufacturing Settings", "allow_production_on_holidays"))):
return schedule_date
if schedule_date in tuple(get_holidays(self.holiday_list)):
schedule_date = add_days(schedule_date, 1)
self.validate_workstation_holiday(schedule_date, skip_holiday_list_check=True)
return schedule_date
@frappe.whitelist() @frappe.whitelist()
def get_default_holiday_list(): def get_default_holiday_list():
return frappe.get_cached_value('Company', frappe.defaults.get_user_default("Company"), "default_holiday_list") return frappe.get_cached_value('Company', frappe.defaults.get_user_default("Company"), "default_holiday_list")

View File

@ -6,8 +6,12 @@ def get_data():
'fieldname': 'workstation', 'fieldname': 'workstation',
'transactions': [ 'transactions': [
{ {
'label': _('Manufacture'), 'label': _('Master'),
'items': ['BOM', 'Routing', 'Work Order', 'Job Card', 'Operation', 'Timesheet'] 'items': ['BOM', 'Routing', 'Operation']
},
{
'label': _('Transaction'),
'items': ['Work Order', 'Job Card', 'Timesheet']
} }
] ]
} }

View File

@ -646,4 +646,5 @@ erpnext.patches.v12_0.set_payment_entry_status
erpnext.patches.v12_0.update_owner_fields_in_acc_dimension_custom_fields erpnext.patches.v12_0.update_owner_fields_in_acc_dimension_custom_fields
erpnext.patches.v12_0.set_default_for_add_taxes_from_item_tax_template erpnext.patches.v12_0.set_default_for_add_taxes_from_item_tax_template
erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger
erpnext.patches.v12_0.update_price_or_product_discount erpnext.patches.v12_0.update_price_or_product_discount
erpnext.patches.v12_0.set_production_capacity_in_workstation

View File

@ -0,0 +1,8 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("manufacturing", "doctype", "workstation")
frappe.db.sql(""" UPDATE `tabWorkstation`
SET production_capacity = 1 """)